mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-24 07:01:49 +00:00
fix(core): restore outbound fallbacks and gate checks
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { CiaoService, Responder } from "@homebridge/ciao";
|
||||
import { logDebug, logWarn } from "../logger.js";
|
||||
import { getLogger } from "../logging.js";
|
||||
import { ignoreCiaoCancellationRejection } from "./bonjour-ciao.js";
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
ExecApprovalRequest,
|
||||
ExecApprovalResolved,
|
||||
} from "./exec-approvals.js";
|
||||
import { resolveBuiltInExecApprovalAdapter } from "./outbound/built-in-channel-adapters.js";
|
||||
import { deliverOutboundPayloads } from "./outbound/deliver.js";
|
||||
|
||||
const log = createSubsystemLogger("gateway/exec-approvals");
|
||||
@@ -118,8 +119,10 @@ function shouldSkipForwardingFallback(params: {
|
||||
if (!channel) {
|
||||
return false;
|
||||
}
|
||||
const adapter =
|
||||
getChannelPlugin(channel)?.execApprovals ?? resolveBuiltInExecApprovalAdapter(channel);
|
||||
return (
|
||||
getChannelPlugin(channel)?.execApprovals?.shouldSuppressForwardingFallback?.({
|
||||
adapter?.shouldSuppressForwardingFallback?.({
|
||||
cfg: params.cfg,
|
||||
target: params.target,
|
||||
request: params.request,
|
||||
@@ -275,7 +278,9 @@ function buildRequestPayloadForTarget(
|
||||
): ReplyPayload {
|
||||
const channel = normalizeMessageChannel(target.channel) ?? target.channel;
|
||||
const pluginPayload = channel
|
||||
? getChannelPlugin(channel)?.execApprovals?.buildPendingPayload?.({
|
||||
? (
|
||||
getChannelPlugin(channel)?.execApprovals ?? resolveBuiltInExecApprovalAdapter(channel)
|
||||
)?.buildPendingPayload?.({
|
||||
cfg,
|
||||
request,
|
||||
target,
|
||||
@@ -410,7 +415,9 @@ export function createExecApprovalForwarder(
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
await getChannelPlugin(channel)?.execApprovals?.beforeDeliverPending?.({
|
||||
await (
|
||||
getChannelPlugin(channel)?.execApprovals ?? resolveBuiltInExecApprovalAdapter(channel)
|
||||
)?.beforeDeliverPending?.({
|
||||
cfg,
|
||||
target,
|
||||
payload,
|
||||
|
||||
127
src/infra/outbound/built-in-channel-adapters.ts
Normal file
127
src/infra/outbound/built-in-channel-adapters.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { Separator, TextDisplay } from "@buape/carbon";
|
||||
import {
|
||||
listDiscordAccountIds,
|
||||
resolveDiscordAccount,
|
||||
} from "../../../extensions/discord/src/accounts.js";
|
||||
import { isDiscordExecApprovalClientEnabled } from "../../../extensions/discord/src/exec-approvals.js";
|
||||
import { DiscordUiContainer } from "../../../extensions/discord/src/ui.js";
|
||||
import { listTelegramAccountIds } from "../../../extensions/telegram/src/accounts.js";
|
||||
import { buildTelegramExecApprovalButtons } from "../../../extensions/telegram/src/approval-buttons.js";
|
||||
import {
|
||||
isTelegramExecApprovalClientEnabled,
|
||||
resolveTelegramExecApprovalTarget,
|
||||
} from "../../../extensions/telegram/src/exec-approvals.js";
|
||||
import type { ChannelExecApprovalAdapter } from "../../channels/plugins/types.adapters.js";
|
||||
import type { ChannelCrossContextComponentsFactory } from "../../channels/plugins/types.core.js";
|
||||
import type { ChannelId } from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { normalizeMessageChannel } from "../../utils/message-channel.js";
|
||||
import { resolveExecApprovalCommandDisplay } from "../exec-approval-command-display.js";
|
||||
import { buildExecApprovalPendingReplyPayload } from "../exec-approval-reply.js";
|
||||
|
||||
const BUILT_IN_DISCORD_CROSS_CONTEXT_COMPONENTS: ChannelCrossContextComponentsFactory = (
|
||||
params,
|
||||
) => {
|
||||
const trimmed = params.message.trim();
|
||||
const components: Array<TextDisplay | Separator> = [];
|
||||
if (trimmed) {
|
||||
components.push(new TextDisplay(params.message));
|
||||
components.push(new Separator({ divider: true, spacing: "small" }));
|
||||
}
|
||||
components.push(new TextDisplay(`*From ${params.originLabel}*`));
|
||||
return [new DiscordUiContainer({ cfg: params.cfg, accountId: params.accountId, components })];
|
||||
};
|
||||
|
||||
function hasDiscordExecApprovalDmRoute(cfg: OpenClawConfig): boolean {
|
||||
return listDiscordAccountIds(cfg).some((accountId) => {
|
||||
const execApprovals = resolveDiscordAccount({ cfg, accountId }).config.execApprovals;
|
||||
if (!execApprovals?.enabled || (execApprovals.approvers?.length ?? 0) === 0) {
|
||||
return false;
|
||||
}
|
||||
const target = execApprovals.target ?? "dm";
|
||||
return target === "dm" || target === "both";
|
||||
});
|
||||
}
|
||||
|
||||
function hasTelegramExecApprovalDmRoute(cfg: OpenClawConfig): boolean {
|
||||
return listTelegramAccountIds(cfg).some((accountId) => {
|
||||
if (!isTelegramExecApprovalClientEnabled({ cfg, accountId })) {
|
||||
return false;
|
||||
}
|
||||
const target = resolveTelegramExecApprovalTarget({ cfg, accountId });
|
||||
return target === "dm" || target === "both";
|
||||
});
|
||||
}
|
||||
|
||||
const BUILT_IN_DISCORD_EXEC_APPROVALS: ChannelExecApprovalAdapter = {
|
||||
getInitiatingSurfaceState: ({ cfg, accountId }) =>
|
||||
isDiscordExecApprovalClientEnabled({ cfg, accountId })
|
||||
? { kind: "enabled" }
|
||||
: { kind: "disabled" },
|
||||
hasConfiguredDmRoute: ({ cfg }) => hasDiscordExecApprovalDmRoute(cfg),
|
||||
shouldSuppressForwardingFallback: ({ cfg, target }) =>
|
||||
(normalizeMessageChannel(target.channel) ?? target.channel) === "discord" &&
|
||||
isDiscordExecApprovalClientEnabled({ cfg, accountId: target.accountId }),
|
||||
};
|
||||
|
||||
const BUILT_IN_TELEGRAM_EXEC_APPROVALS: ChannelExecApprovalAdapter = {
|
||||
getInitiatingSurfaceState: ({ cfg, accountId }) =>
|
||||
isTelegramExecApprovalClientEnabled({ cfg, accountId })
|
||||
? { kind: "enabled" }
|
||||
: { kind: "disabled" },
|
||||
hasConfiguredDmRoute: ({ cfg }) => hasTelegramExecApprovalDmRoute(cfg),
|
||||
shouldSuppressForwardingFallback: ({ cfg, target, request }) => {
|
||||
const channel = normalizeMessageChannel(target.channel) ?? target.channel;
|
||||
if (channel !== "telegram") {
|
||||
return false;
|
||||
}
|
||||
const requestChannel = normalizeMessageChannel(request.request.turnSourceChannel ?? "");
|
||||
if (requestChannel !== "telegram") {
|
||||
return false;
|
||||
}
|
||||
const accountId = target.accountId?.trim() || request.request.turnSourceAccountId?.trim();
|
||||
return isTelegramExecApprovalClientEnabled({ cfg, accountId });
|
||||
},
|
||||
buildPendingPayload: ({ request, nowMs }) => {
|
||||
const payload = buildExecApprovalPendingReplyPayload({
|
||||
approvalId: request.id,
|
||||
approvalSlug: request.id.slice(0, 8),
|
||||
approvalCommandId: request.id,
|
||||
command: resolveExecApprovalCommandDisplay(request.request).commandText,
|
||||
cwd: request.request.cwd ?? undefined,
|
||||
host: request.request.host === "node" ? "node" : "gateway",
|
||||
nodeId: request.request.nodeId ?? undefined,
|
||||
expiresAtMs: request.expiresAtMs,
|
||||
nowMs,
|
||||
});
|
||||
const buttons = buildTelegramExecApprovalButtons(request.id);
|
||||
if (!buttons) {
|
||||
return payload;
|
||||
}
|
||||
return {
|
||||
...payload,
|
||||
channelData: {
|
||||
...payload.channelData,
|
||||
telegram: { buttons },
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export function resolveBuiltInCrossContextComponentsFactory(
|
||||
channel: ChannelId,
|
||||
): ChannelCrossContextComponentsFactory | undefined {
|
||||
return channel === "discord" ? BUILT_IN_DISCORD_CROSS_CONTEXT_COMPONENTS : undefined;
|
||||
}
|
||||
|
||||
export function resolveBuiltInExecApprovalAdapter(
|
||||
channel: ChannelId,
|
||||
): ChannelExecApprovalAdapter | undefined {
|
||||
if (channel === "discord") {
|
||||
return BUILT_IN_DISCORD_EXEC_APPROVALS;
|
||||
}
|
||||
if (channel === "telegram") {
|
||||
return BUILT_IN_TELEGRAM_EXEC_APPROVALS;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
41
src/infra/outbound/built-in-channel-messaging.ts
Normal file
41
src/infra/outbound/built-in-channel-messaging.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { parseTelegramTarget } from "../../../extensions/telegram/src/targets.js";
|
||||
import type { ChatType } from "../../channels/chat-type.js";
|
||||
import type { ChannelId } from "../../channels/plugins/types.js";
|
||||
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../whatsapp/normalize.js";
|
||||
|
||||
export type BuiltInExplicitTarget = {
|
||||
to: string;
|
||||
threadId?: string | number;
|
||||
chatType?: ChatType;
|
||||
};
|
||||
|
||||
export function resolveBuiltInExplicitTarget(
|
||||
channel: ChannelId,
|
||||
raw: string,
|
||||
): BuiltInExplicitTarget | null {
|
||||
if (channel === "telegram") {
|
||||
const target = parseTelegramTarget(raw);
|
||||
return {
|
||||
to: target.chatId,
|
||||
threadId: target.messageThreadId,
|
||||
chatType: target.chatType === "unknown" ? undefined : target.chatType,
|
||||
};
|
||||
}
|
||||
|
||||
if (channel === "whatsapp") {
|
||||
const normalized = normalizeWhatsAppTarget(raw);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
to: normalized,
|
||||
chatType: isWhatsAppGroupJid(normalized) ? "group" : "direct",
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveBuiltInTargetChatType(channel: ChannelId, to: string): ChatType | undefined {
|
||||
return resolveBuiltInExplicitTarget(channel, to)?.chatType;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { TopLevelComponents } from "@buape/carbon";
|
||||
import { getChannelPlugin } from "../../channels/plugins/index.js";
|
||||
import type { ChannelId } from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolveBuiltInCrossContextComponentsFactory } from "./built-in-channel-adapters.js";
|
||||
|
||||
export type CrossContextComponentsBuilder = (message: string) => TopLevelComponents[];
|
||||
|
||||
@@ -22,7 +23,9 @@ const DEFAULT_ADAPTER: ChannelMessageAdapter = {
|
||||
};
|
||||
|
||||
export function getChannelMessageAdapter(channel: ChannelId): ChannelMessageAdapter {
|
||||
const adapter = getChannelPlugin(channel)?.messaging?.buildCrossContextComponents;
|
||||
const adapter =
|
||||
getChannelPlugin(channel)?.messaging?.buildCrossContextComponents ??
|
||||
resolveBuiltInCrossContextComponentsFactory(channel);
|
||||
if (adapter) {
|
||||
return {
|
||||
supportsComponentsV2: true,
|
||||
|
||||
@@ -387,6 +387,24 @@ describe("resolveSessionDeliveryTarget", () => {
|
||||
expect(resolved.threadId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps :topic: parsing when the telegram plugin registry is unavailable", () => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
|
||||
const resolved = resolveSessionDeliveryTarget({
|
||||
entry: {
|
||||
sessionId: "sess-no-registry",
|
||||
updatedAt: 1,
|
||||
lastChannel: "telegram",
|
||||
lastTo: "63448508",
|
||||
},
|
||||
requestedChannel: "last",
|
||||
explicitTo: "63448508:topic:1008013",
|
||||
});
|
||||
|
||||
expect(resolved.to).toBe("63448508");
|
||||
expect(resolved.threadId).toBe(1008013);
|
||||
});
|
||||
|
||||
it("explicitThreadId takes priority over :topic: parsed value", () => {
|
||||
const resolved = resolveSessionDeliveryTarget({
|
||||
entry: {
|
||||
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
isDeliverableMessageChannel,
|
||||
normalizeMessageChannel,
|
||||
} from "../../utils/message-channel.js";
|
||||
import {
|
||||
resolveBuiltInExplicitTarget,
|
||||
resolveBuiltInTargetChatType,
|
||||
} from "./built-in-channel-messaging.js";
|
||||
import {
|
||||
normalizeDeliverableOutboundChannel,
|
||||
resolveOutboundChannelPlugin,
|
||||
@@ -74,7 +78,7 @@ function parseExplicitTargetWithPlugin(params: {
|
||||
return (
|
||||
resolveOutboundChannelPlugin({ channel: provider })?.messaging?.parseExplicitTarget?.({
|
||||
raw,
|
||||
}) ?? null
|
||||
}) ?? resolveBuiltInExplicitTarget(provider, raw)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -415,9 +419,11 @@ function inferChatTypeFromTarget(params: {
|
||||
if (/^group:/i.test(to)) {
|
||||
return "group";
|
||||
}
|
||||
return resolveOutboundChannelPlugin({
|
||||
channel: params.channel,
|
||||
})?.messaging?.inferTargetChatType?.({ to });
|
||||
return (
|
||||
resolveOutboundChannelPlugin({
|
||||
channel: params.channel,
|
||||
})?.messaging?.inferTargetChatType?.({ to }) ?? resolveBuiltInTargetChatType(params.channel, to)
|
||||
);
|
||||
}
|
||||
|
||||
function resolveHeartbeatDeliveryChatType(params: {
|
||||
|
||||
@@ -15,6 +15,10 @@ type ScrollHost = {
|
||||
topbarObserver: ResizeObserver | null;
|
||||
};
|
||||
|
||||
function queryHost(host: Partial<ScrollHost>, selectors: string): Element | null {
|
||||
return typeof host.querySelector === "function" ? host.querySelector(selectors) : null;
|
||||
}
|
||||
|
||||
export function scheduleChatScroll(host: ScrollHost, force = false, smooth = false) {
|
||||
if (host.chatScrollFrame) {
|
||||
cancelAnimationFrame(host.chatScrollFrame);
|
||||
@@ -24,7 +28,7 @@ export function scheduleChatScroll(host: ScrollHost, force = false, smooth = fal
|
||||
host.chatScrollTimeout = null;
|
||||
}
|
||||
const pickScrollTarget = () => {
|
||||
const container = host.querySelector(".chat-thread") as HTMLElement | null;
|
||||
const container = queryHost(host, ".chat-thread") as HTMLElement | null;
|
||||
if (container) {
|
||||
const overflowY = getComputedStyle(container).overflowY;
|
||||
const canScroll =
|
||||
@@ -104,7 +108,7 @@ export function scheduleLogsScroll(host: ScrollHost, force = false) {
|
||||
void host.updateComplete.then(() => {
|
||||
host.logsScrollFrame = requestAnimationFrame(() => {
|
||||
host.logsScrollFrame = null;
|
||||
const container = host.querySelector(".log-stream") as HTMLElement | null;
|
||||
const container = queryHost(host, ".log-stream") as HTMLElement | null;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
@@ -165,7 +169,7 @@ export function observeTopbar(host: ScrollHost) {
|
||||
if (typeof ResizeObserver === "undefined") {
|
||||
return;
|
||||
}
|
||||
const topbar = host.querySelector(".topbar");
|
||||
const topbar = queryHost(host, ".topbar");
|
||||
if (!topbar) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user