mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
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:
committed by
GitHub
parent
a4850b1b8f
commit
21e8d88c1d
@@ -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.
|
||||
|
||||
1
src/agents/command-poll-backoff.runtime.ts
Normal file
1
src/agents/command-poll-backoff.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { pruneStaleCommandPolls } from "./command-poll-backoff.js";
|
||||
7
src/agents/pi-tools.before-tool-call.runtime.ts
Normal file
7
src/agents/pi-tools.before-tool-call.runtime.ts
Normal 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";
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
7
src/agents/subagent-registry-runtime.ts
Normal file
7
src/agents/subagent-registry-runtime.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
countActiveDescendantRuns,
|
||||
countPendingDescendantRuns,
|
||||
countPendingDescendantRunsExcludingRun,
|
||||
isSubagentSessionRunActive,
|
||||
resolveRequesterForChildSession,
|
||||
} from "./subagent-registry.js";
|
||||
@@ -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,
|
||||
|
||||
1
src/cli/deps-send-discord.runtime.ts
Normal file
1
src/cli/deps-send-discord.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { sendMessageDiscord } from "../discord/send.js";
|
||||
1
src/cli/deps-send-imessage.runtime.ts
Normal file
1
src/cli/deps-send-imessage.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { sendMessageIMessage } from "../imessage/send.js";
|
||||
1
src/cli/deps-send-signal.runtime.ts
Normal file
1
src/cli/deps-send-signal.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { sendMessageSignal } from "../signal/send.js";
|
||||
1
src/cli/deps-send-slack.runtime.ts
Normal file
1
src/cli/deps-send-slack.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { sendMessageSlack } from "../slack/send.js";
|
||||
1
src/cli/deps-send-telegram.runtime.ts
Normal file
1
src/cli/deps-send-telegram.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { sendMessageTelegram } from "../telegram/send.js";
|
||||
1
src/cli/deps-send-whatsapp.runtime.ts
Normal file
1
src/cli/deps-send-whatsapp.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { sendMessageWhatsApp } from "../channels/web/index.js";
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
1
src/infra/outbound/deliver-runtime.ts
Normal file
1
src/infra/outbound/deliver-runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { deliverOutboundPayloads } from "./deliver.js";
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
1
src/media-understanding/providers/image-runtime.ts
Normal file
1
src/media-understanding/providers/image-runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { describeImageWithModel } from "./image.js";
|
||||
1
src/memory/manager-runtime.ts
Normal file
1
src/memory/manager-runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { MemoryIndexManager } from "./manager.js";
|
||||
@@ -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) {
|
||||
|
||||
1
src/plugins/runtime/runtime-whatsapp-login.runtime.ts
Normal file
1
src/plugins/runtime/runtime-whatsapp-login.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { loginWeb } from "../../web/login.js";
|
||||
1
src/plugins/runtime/runtime-whatsapp-outbound.runtime.ts
Normal file
1
src/plugins/runtime/runtime-whatsapp-outbound.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { sendMessageWhatsApp, sendPollWhatsApp } from "../../web/outbound.js";
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
7
src/slack/monitor/slash-commands.runtime.ts
Normal file
7
src/slack/monitor/slash-commands.runtime.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
buildCommandTextFromArgs,
|
||||
findCommandByNativeName,
|
||||
listNativeCommandSpecsForConfig,
|
||||
parseCommandArgs,
|
||||
resolveCommandArgMenu,
|
||||
} from "../../auto-reply/commands-registry.js";
|
||||
9
src/slack/monitor/slash-dispatch.runtime.ts
Normal file
9
src/slack/monitor/slash-dispatch.runtime.ts
Normal 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";
|
||||
1
src/slack/monitor/slash-skill-commands.runtime.ts
Normal file
1
src/slack/monitor/slash-skill-commands.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { listSkillCommandsForAgents } from "../../auto-reply/skill-commands.js";
|
||||
@@ -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 =
|
||||
|
||||
74
src/telegram/audit-membership-runtime.ts
Normal file
74
src/telegram/audit-membership-runtime.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user