mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-31 13:38:35 +00:00
refactor: dedupe channel approval forwarding
This commit is contained in:
@@ -1,23 +1,18 @@
|
||||
import {
|
||||
buildChannelApprovalExpiredText,
|
||||
buildChannelApprovalResolvedText,
|
||||
createChannelApprovalNativeRuntimeAdapter,
|
||||
type ExpiredApprovalView,
|
||||
type PendingApprovalView,
|
||||
type ResolvedApprovalView,
|
||||
resolvePreparedApprovalAccountId,
|
||||
} from "openclaw/plugin-sdk/approval-handler-runtime";
|
||||
import { buildChannelApprovalNativeTargetKey } from "openclaw/plugin-sdk/approval-native-runtime";
|
||||
import { buildApprovalReactionPendingContent } from "openclaw/plugin-sdk/approval-reaction-runtime";
|
||||
import type { ExecApprovalReplyDecision } from "openclaw/plugin-sdk/approval-reply-runtime";
|
||||
import {
|
||||
buildApprovalResolvedReplyPayload,
|
||||
buildPluginApprovalExpiredMessage,
|
||||
buildPluginApprovalResolvedMessage,
|
||||
type ExecApprovalRequest,
|
||||
type ExecApprovalResolved,
|
||||
type PluginApprovalRequest,
|
||||
type PluginApprovalResolved,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
registerIMessageApprovalReactionTarget,
|
||||
unregisterIMessageApprovalReactionTarget,
|
||||
@@ -30,7 +25,6 @@ import { normalizeIMessageHandle, parseIMessageTarget } from "./targets.js";
|
||||
const log = createSubsystemLogger("imessage/approvals");
|
||||
|
||||
type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest;
|
||||
type ApprovalResolved = ExecApprovalResolved | PluginApprovalResolved;
|
||||
type IMessagePendingDelivery = {
|
||||
text: string;
|
||||
allowedDecisions: readonly ExecApprovalReplyDecision[];
|
||||
@@ -66,42 +60,6 @@ function buildPendingPayload(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function buildResolvedText(params: {
|
||||
request: ApprovalRequest;
|
||||
resolved: ApprovalResolved;
|
||||
view: ResolvedApprovalView;
|
||||
}): string {
|
||||
if (params.view.approvalKind === "plugin") {
|
||||
return buildPluginApprovalResolvedMessage(params.resolved as PluginApprovalResolved);
|
||||
}
|
||||
const resolvedByText = params.resolved.resolvedBy
|
||||
? ` Resolved by ${params.resolved.resolvedBy}.`
|
||||
: "";
|
||||
const payload = buildApprovalResolvedReplyPayload({
|
||||
approvalId: params.request.id,
|
||||
approvalSlug: params.request.id.slice(0, 8),
|
||||
text: `✅ Exec approval ${params.resolved.decision}.${resolvedByText} ID: ${params.request.id}`,
|
||||
});
|
||||
return payload.text ?? "";
|
||||
}
|
||||
|
||||
function buildExpiredText(params: { request: ApprovalRequest; view: ExpiredApprovalView }): string {
|
||||
if (params.view.approvalKind === "plugin") {
|
||||
return buildPluginApprovalExpiredMessage(params.request as PluginApprovalRequest);
|
||||
}
|
||||
return `⏱️ Exec approval expired. ID: ${params.request.id}`;
|
||||
}
|
||||
|
||||
function resolvePreparedAccountId(params: {
|
||||
plannedAccountId?: string | null;
|
||||
contextAccountId?: string | null;
|
||||
}): string | undefined {
|
||||
return (
|
||||
normalizeOptionalString(params.plannedAccountId) ??
|
||||
normalizeOptionalString(params.contextAccountId)
|
||||
);
|
||||
}
|
||||
|
||||
function buildConversationKeyForTarget(to: string): IMessageApprovalConversationKey | null {
|
||||
try {
|
||||
const parsed = parseIMessageTarget(to);
|
||||
@@ -138,11 +96,11 @@ export const imessageApprovalNativeRuntime = createChannelApprovalNativeRuntimeA
|
||||
buildPendingPayload({ request, approvalKind, nowMs, view }),
|
||||
buildResolvedResult: ({ request, resolved, view }) => ({
|
||||
kind: "update",
|
||||
payload: { text: buildResolvedText({ request, resolved, view }) },
|
||||
payload: { text: buildChannelApprovalResolvedText({ request, resolved, view }) },
|
||||
}),
|
||||
buildExpiredResult: ({ request, view }) => ({
|
||||
kind: "update",
|
||||
payload: { text: buildExpiredText({ request, view }) },
|
||||
payload: { text: buildChannelApprovalExpiredText({ request, view }) },
|
||||
}),
|
||||
},
|
||||
transport: {
|
||||
@@ -153,7 +111,7 @@ export const imessageApprovalNativeRuntime = createChannelApprovalNativeRuntimeA
|
||||
}
|
||||
const prepared: PreparedIMessageApprovalTarget = {
|
||||
to,
|
||||
accountId: resolvePreparedAccountId({
|
||||
accountId: resolvePreparedApprovalAccountId({
|
||||
plannedAccountId: (plannedTarget.target as { accountId?: string | null }).accountId,
|
||||
contextAccountId: accountId,
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { matchesApprovalRequestFilters } from "openclaw/plugin-sdk/approval-client-runtime";
|
||||
import {
|
||||
createChannelApprovalCapability,
|
||||
splitChannelApprovalCapability,
|
||||
@@ -6,9 +5,11 @@ import {
|
||||
import { createLazyChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-adapter-runtime";
|
||||
import type { ChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime";
|
||||
import {
|
||||
createChannelApprovalForwardingEvaluator,
|
||||
createChannelApproverDmTargetResolver,
|
||||
createChannelNativeOriginTargetResolver,
|
||||
doesApprovalRequestMatchChannelAccount,
|
||||
createNativeApprovalForwardingFallbackSuppressor,
|
||||
nativeApprovalTargetsMatch,
|
||||
resolveApprovalRequestSessionTarget,
|
||||
shouldSuppressLocalNativeExecApprovalPrompt,
|
||||
} from "openclaw/plugin-sdk/approval-native-runtime";
|
||||
@@ -28,7 +29,6 @@ import type {
|
||||
ChannelApprovalCapability,
|
||||
ChannelOutboundPayloadHint,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import { channelRouteTargetsMatchExact } from "openclaw/plugin-sdk/channel-route";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { normalizeAccountId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
@@ -50,7 +50,6 @@ import { inferIMessageTargetChatType } from "./targets.js";
|
||||
type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest;
|
||||
type ApprovalKind = "exec" | "plugin";
|
||||
type ApprovalForwardingConfig = NonNullable<NonNullable<OpenClawConfig["approvals"]>["exec"]>;
|
||||
type ApprovalForwardingMode = NonNullable<ApprovalForwardingConfig["mode"]>;
|
||||
type ChannelApprovalForwardTarget = Parameters<
|
||||
NonNullable<
|
||||
NonNullable<ChannelApprovalCapability["delivery"]>["shouldSuppressForwardingFallback"]
|
||||
@@ -62,7 +61,6 @@ type IMessageApprovalTarget = {
|
||||
threadId?: string | number | null;
|
||||
};
|
||||
|
||||
const DEFAULT_APPROVAL_FORWARDING_MODE: ApprovalForwardingMode = "session";
|
||||
const DEFAULT_PLUGIN_APPROVAL_DECISIONS: readonly ExecApprovalReplyDecision[] = [
|
||||
"allow-once",
|
||||
"allow-always",
|
||||
@@ -76,48 +74,6 @@ function isIMessageApprovalTransportEnabled(params: {
|
||||
return resolveIMessageAccount({ cfg: params.cfg, accountId: params.accountId }).enabled;
|
||||
}
|
||||
|
||||
function resolveApprovalKind(request: ApprovalRequest, approvalKind?: ApprovalKind): ApprovalKind {
|
||||
if (approvalKind) {
|
||||
return approvalKind;
|
||||
}
|
||||
return "command" in request.request ? "exec" : "plugin";
|
||||
}
|
||||
|
||||
function resolveApprovalForwardingConfig(params: {
|
||||
cfg: OpenClawConfig;
|
||||
approvalKind: ApprovalKind;
|
||||
}): ApprovalForwardingConfig | undefined {
|
||||
return params.approvalKind === "plugin"
|
||||
? params.cfg.approvals?.plugin
|
||||
: params.cfg.approvals?.exec;
|
||||
}
|
||||
|
||||
function normalizeApprovalForwardingMode(
|
||||
mode: ApprovalForwardingConfig["mode"] | undefined,
|
||||
): ApprovalForwardingMode {
|
||||
return mode ?? DEFAULT_APPROVAL_FORWARDING_MODE;
|
||||
}
|
||||
|
||||
function approvalModeIncludesSession(mode: ApprovalForwardingMode): boolean {
|
||||
return mode === "session" || mode === "both";
|
||||
}
|
||||
|
||||
function approvalModeIncludesTargets(mode: ApprovalForwardingMode): boolean {
|
||||
return mode === "targets" || mode === "both";
|
||||
}
|
||||
|
||||
function matchesForwardingFilters(params: {
|
||||
config: ApprovalForwardingConfig;
|
||||
request: ApprovalRequest;
|
||||
}): boolean {
|
||||
return matchesApprovalRequestFilters({
|
||||
request: params.request.request,
|
||||
agentFilter: params.config.agentFilter,
|
||||
sessionFilter: params.config.sessionFilter,
|
||||
fallbackAgentIdFromSessionKey: true,
|
||||
});
|
||||
}
|
||||
|
||||
function targetAccountMatchesIMessageAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
targetAccountId?: string | null;
|
||||
@@ -164,26 +120,6 @@ function normalizeIMessageForwardTarget(
|
||||
};
|
||||
}
|
||||
|
||||
function nativeApprovalTargetsMatch(params: {
|
||||
left: IMessageApprovalTarget;
|
||||
right: IMessageApprovalTarget;
|
||||
}): boolean {
|
||||
return channelRouteTargetsMatchExact({
|
||||
left: {
|
||||
channel: "imessage",
|
||||
to: params.left.to,
|
||||
accountId: params.left.accountId,
|
||||
threadId: params.left.threadId,
|
||||
},
|
||||
right: {
|
||||
channel: "imessage",
|
||||
to: params.right.to,
|
||||
accountId: params.right.accountId,
|
||||
threadId: params.right.threadId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function hasMatchingIMessageTarget(params: {
|
||||
cfg: OpenClawConfig;
|
||||
config: ApprovalForwardingConfig;
|
||||
@@ -208,7 +144,11 @@ function hasMatchingIMessageTarget(params: {
|
||||
if (!candidateTarget) {
|
||||
return true;
|
||||
}
|
||||
return nativeApprovalTargetsMatch({ left: configuredTarget, right: candidateTarget });
|
||||
return nativeApprovalTargetsMatch({
|
||||
channel: "imessage",
|
||||
left: configuredTarget,
|
||||
right: candidateTarget,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -235,118 +175,17 @@ function hasIMessageOriginOrSessionTarget(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function canApprovalPotentiallyRouteToIMessage(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
approvalKind: ApprovalKind;
|
||||
nativeSessionOnly?: boolean;
|
||||
}): boolean {
|
||||
if (!isIMessageApprovalTransportEnabled(params)) {
|
||||
return false;
|
||||
}
|
||||
const config = resolveApprovalForwardingConfig(params);
|
||||
if (!config?.enabled) {
|
||||
return false;
|
||||
}
|
||||
const mode = normalizeApprovalForwardingMode(config.mode);
|
||||
if (approvalModeIncludesSession(mode)) {
|
||||
return true;
|
||||
}
|
||||
if (params.nativeSessionOnly) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
approvalModeIncludesTargets(mode) &&
|
||||
hasMatchingIMessageTarget({
|
||||
cfg: params.cfg,
|
||||
config,
|
||||
accountId: params.accountId,
|
||||
})
|
||||
);
|
||||
}
|
||||
const imessageApprovalForwarding = createChannelApprovalForwardingEvaluator({
|
||||
channel: "imessage",
|
||||
isTransportEnabled: isIMessageApprovalTransportEnabled,
|
||||
hasMatchingTarget: hasMatchingIMessageTarget,
|
||||
hasOriginOrSessionTarget: hasIMessageOriginOrSessionTarget,
|
||||
});
|
||||
|
||||
function canAnyApprovalPotentiallyRouteToIMessage(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
nativeSessionOnly?: boolean;
|
||||
}): boolean {
|
||||
return (
|
||||
canApprovalPotentiallyRouteToIMessage({
|
||||
...params,
|
||||
approvalKind: "exec",
|
||||
}) ||
|
||||
canApprovalPotentiallyRouteToIMessage({
|
||||
...params,
|
||||
approvalKind: "plugin",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function isIMessageSessionApprovalEligible(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
approvalKind: ApprovalKind;
|
||||
request: ApprovalRequest;
|
||||
}): boolean {
|
||||
if (!isIMessageApprovalTransportEnabled(params)) {
|
||||
return false;
|
||||
}
|
||||
const config = resolveApprovalForwardingConfig(params);
|
||||
if (!config?.enabled) {
|
||||
return false;
|
||||
}
|
||||
const mode = normalizeApprovalForwardingMode(config.mode);
|
||||
if (!approvalModeIncludesSession(mode)) {
|
||||
return false;
|
||||
}
|
||||
if (!matchesForwardingFilters({ config, request: params.request })) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!doesApprovalRequestMatchChannelAccount({
|
||||
cfg: params.cfg,
|
||||
request: params.request,
|
||||
channel: "imessage",
|
||||
accountId: params.accountId,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return hasIMessageOriginOrSessionTarget({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
request: params.request,
|
||||
});
|
||||
}
|
||||
|
||||
function isIMessageExplicitTargetEligible(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
approvalKind: ApprovalKind;
|
||||
request: ApprovalRequest;
|
||||
target: ChannelApprovalForwardTarget;
|
||||
}): boolean {
|
||||
if (!isIMessageApprovalTransportEnabled(params)) {
|
||||
return false;
|
||||
}
|
||||
const config = resolveApprovalForwardingConfig(params);
|
||||
if (!config?.enabled) {
|
||||
return false;
|
||||
}
|
||||
const mode = normalizeApprovalForwardingMode(config.mode);
|
||||
if (!approvalModeIncludesTargets(mode)) {
|
||||
return false;
|
||||
}
|
||||
if (!matchesForwardingFilters({ config, request: params.request })) {
|
||||
return false;
|
||||
}
|
||||
return hasMatchingIMessageTarget({
|
||||
cfg: params.cfg,
|
||||
config,
|
||||
accountId: params.accountId,
|
||||
target: params.target,
|
||||
});
|
||||
}
|
||||
const canApprovalPotentiallyRouteToIMessage = imessageApprovalForwarding.isPotentialRoute;
|
||||
const canAnyApprovalPotentiallyRouteToIMessage = imessageApprovalForwarding.canAnyPotentiallyRoute;
|
||||
const isIMessageSessionApprovalEligible = imessageApprovalForwarding.isSessionEligible;
|
||||
const isIMessageExplicitTargetEligible = imessageApprovalForwarding.isExplicitTargetEligible;
|
||||
|
||||
function resolveTurnSourceIMessageOriginTarget(
|
||||
request: ApprovalRequest,
|
||||
@@ -475,10 +314,7 @@ function shouldHandleIMessageApprovalRequest(params: {
|
||||
approvalKind?: ApprovalKind;
|
||||
request: ApprovalRequest;
|
||||
}): boolean {
|
||||
return isIMessageSessionApprovalEligible({
|
||||
...params,
|
||||
approvalKind: resolveApprovalKind(params.request, params.approvalKind),
|
||||
});
|
||||
return imessageApprovalForwarding.shouldHandleRequest(params);
|
||||
}
|
||||
|
||||
const resolveIMessageOriginTargetBase = createChannelNativeOriginTargetResolver({
|
||||
@@ -528,6 +364,22 @@ const resolveIMessageApproverDmTargets = createChannelApproverDmTargetResolver({
|
||||
},
|
||||
});
|
||||
|
||||
const shouldSuppressIMessageForwardingFallback =
|
||||
createNativeApprovalForwardingFallbackSuppressor<IMessageApprovalTarget>({
|
||||
channel: "imessage",
|
||||
normalizeForwardTarget: normalizeIMessageForwardTarget,
|
||||
resolveAccountId: ({ forwardingTarget, request }) =>
|
||||
forwardingTarget.accountId ?? normalizeOptionalString(request.request.turnSourceAccountId),
|
||||
resolveForwardingTargetForMatch: ({ forwardingTarget, accountId }) => ({
|
||||
...forwardingTarget,
|
||||
accountId,
|
||||
}),
|
||||
isSessionRouteEligible: isIMessageSessionApprovalEligible,
|
||||
isExplicitTargetEligible: isIMessageExplicitTargetEligible,
|
||||
resolveOriginTarget: resolveIMessageOriginTarget,
|
||||
resolveApproverDmTargets: resolveIMessageApproverDmTargets,
|
||||
});
|
||||
|
||||
function appendIMessageReactionHint(params: {
|
||||
text?: string;
|
||||
allowedDecisions: readonly ExecApprovalReplyDecision[];
|
||||
@@ -625,58 +477,7 @@ export const imessageApprovalCapability: ChannelApprovalCapability =
|
||||
}
|
||||
return getIMessageApprovalApprovers({ cfg, accountId }).length > 0;
|
||||
}),
|
||||
shouldSuppressForwardingFallback: ({ cfg, approvalKind, target, request }) => {
|
||||
const forwardingTarget = normalizeIMessageForwardTarget(target);
|
||||
if (!forwardingTarget) {
|
||||
return false;
|
||||
}
|
||||
const accountId =
|
||||
forwardingTarget.accountId ??
|
||||
normalizeOptionalString(request.request.turnSourceAccountId);
|
||||
const forwardingTargetForMatch = {
|
||||
...forwardingTarget,
|
||||
accountId,
|
||||
};
|
||||
const kind = resolveApprovalKind(request, approvalKind);
|
||||
const eligible =
|
||||
target.source === "target"
|
||||
? isIMessageExplicitTargetEligible({
|
||||
cfg,
|
||||
accountId,
|
||||
approvalKind: kind,
|
||||
request,
|
||||
target,
|
||||
})
|
||||
: isIMessageSessionApprovalEligible({
|
||||
cfg,
|
||||
accountId,
|
||||
approvalKind: kind,
|
||||
request,
|
||||
});
|
||||
if (!eligible) {
|
||||
return false;
|
||||
}
|
||||
const originTarget = resolveIMessageOriginTarget({
|
||||
cfg,
|
||||
accountId,
|
||||
approvalKind: kind,
|
||||
request,
|
||||
});
|
||||
if (
|
||||
originTarget &&
|
||||
nativeApprovalTargetsMatch({ left: forwardingTargetForMatch, right: originTarget })
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return resolveIMessageApproverDmTargets({
|
||||
cfg,
|
||||
accountId,
|
||||
approvalKind: kind,
|
||||
request,
|
||||
}).some((approverTarget) =>
|
||||
nativeApprovalTargetsMatch({ left: forwardingTargetForMatch, right: approverTarget }),
|
||||
);
|
||||
},
|
||||
shouldSuppressForwardingFallback: shouldSuppressIMessageForwardingFallback,
|
||||
},
|
||||
render: {
|
||||
exec: {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
import {
|
||||
buildChannelApprovalExpiredText,
|
||||
buildChannelApprovalResolvedText,
|
||||
createChannelApprovalNativeRuntimeAdapter,
|
||||
type ExpiredApprovalView,
|
||||
type PendingApprovalView,
|
||||
type ResolvedApprovalView,
|
||||
resolvePreparedApprovalAccountId,
|
||||
} from "openclaw/plugin-sdk/approval-handler-runtime";
|
||||
import { buildChannelApprovalNativeTargetKey } from "openclaw/plugin-sdk/approval-native-runtime";
|
||||
import {
|
||||
@@ -11,13 +12,8 @@ import {
|
||||
type ApprovalReactionPendingContent,
|
||||
} from "openclaw/plugin-sdk/approval-reaction-runtime";
|
||||
import {
|
||||
buildApprovalResolvedReplyPayload,
|
||||
buildPluginApprovalExpiredMessage,
|
||||
buildPluginApprovalResolvedMessage,
|
||||
type ExecApprovalRequest,
|
||||
type ExecApprovalResolved,
|
||||
type PluginApprovalRequest,
|
||||
type PluginApprovalResolved,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
@@ -34,7 +30,6 @@ import { sendMessageSignal, sendTypingSignal } from "./send.js";
|
||||
const log = createSubsystemLogger("signal/approvals");
|
||||
|
||||
type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest;
|
||||
type ApprovalResolved = ExecApprovalResolved | PluginApprovalResolved;
|
||||
type SignalPendingDelivery = ApprovalReactionPendingContent;
|
||||
type PreparedSignalApprovalTarget = {
|
||||
to: string;
|
||||
@@ -89,43 +84,6 @@ function buildPendingPayload(params: {
|
||||
return buildApprovalReactionPendingContent(params);
|
||||
}
|
||||
|
||||
function buildResolvedText(params: {
|
||||
request: ApprovalRequest;
|
||||
resolved: ApprovalResolved;
|
||||
view: ResolvedApprovalView;
|
||||
}): string {
|
||||
if (params.view.approvalKind === "plugin") {
|
||||
return buildPluginApprovalResolvedMessage(params.resolved as PluginApprovalResolved);
|
||||
}
|
||||
const resolvedByText = params.resolved.resolvedBy
|
||||
? ` Resolved by ${params.resolved.resolvedBy}.`
|
||||
: "";
|
||||
const payload = buildApprovalResolvedReplyPayload({
|
||||
approvalId: params.request.id,
|
||||
approvalSlug: params.request.id.slice(0, 8),
|
||||
text: `✅ Exec approval ${params.resolved.decision}.${resolvedByText} ID: ${params.request.id}`,
|
||||
});
|
||||
return payload.text ?? "";
|
||||
}
|
||||
|
||||
function buildExpiredText(params: { request: ApprovalRequest; view: ExpiredApprovalView }): string {
|
||||
if (params.view.approvalKind === "plugin") {
|
||||
return buildPluginApprovalExpiredMessage(params.request as PluginApprovalRequest);
|
||||
}
|
||||
return `⏱️ Exec approval expired. ID: ${params.request.id}`;
|
||||
}
|
||||
|
||||
function resolvePreparedAccountId(params: {
|
||||
plannedAccountId?: string | null;
|
||||
contextAccountId?: string | null;
|
||||
}): string {
|
||||
return (
|
||||
normalizeOptionalString(params.plannedAccountId) ??
|
||||
normalizeOptionalString(params.contextAccountId) ??
|
||||
DEFAULT_ACCOUNT_ID
|
||||
);
|
||||
}
|
||||
|
||||
export const signalApprovalNativeRuntime = createChannelApprovalNativeRuntimeAdapter<
|
||||
SignalPendingDelivery,
|
||||
PreparedSignalApprovalTarget,
|
||||
@@ -143,11 +101,11 @@ export const signalApprovalNativeRuntime = createChannelApprovalNativeRuntimeAda
|
||||
buildPendingPayload({ request, nowMs, view }),
|
||||
buildResolvedResult: ({ request, resolved, view }) => ({
|
||||
kind: "update",
|
||||
payload: { text: buildResolvedText({ request, resolved, view }) },
|
||||
payload: { text: buildChannelApprovalResolvedText({ request, resolved, view }) },
|
||||
}),
|
||||
buildExpiredResult: ({ request, view }) => ({
|
||||
kind: "update",
|
||||
payload: { text: buildExpiredText({ request, view }) },
|
||||
payload: { text: buildChannelApprovalExpiredText({ request, view }) },
|
||||
}),
|
||||
},
|
||||
transport: {
|
||||
@@ -163,9 +121,10 @@ export const signalApprovalNativeRuntime = createChannelApprovalNativeRuntimeAda
|
||||
});
|
||||
const prepared: PreparedSignalApprovalTarget = {
|
||||
to,
|
||||
accountId: resolvePreparedAccountId({
|
||||
accountId: resolvePreparedApprovalAccountId({
|
||||
plannedAccountId: (plannedTarget.target as { accountId?: string | null }).accountId,
|
||||
contextAccountId: accountId,
|
||||
fallbackAccountId: DEFAULT_ACCOUNT_ID,
|
||||
}),
|
||||
...(runtimeContext.baseUrl ? { baseUrl: runtimeContext.baseUrl } : {}),
|
||||
...(runtimeContext.account ? { account: runtimeContext.account } : {}),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { matchesApprovalRequestFilters } from "openclaw/plugin-sdk/approval-client-runtime";
|
||||
import {
|
||||
createChannelApprovalCapability,
|
||||
splitChannelApprovalCapability,
|
||||
@@ -6,10 +5,10 @@ import {
|
||||
import { createLazyChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-adapter-runtime";
|
||||
import type { ChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime";
|
||||
import {
|
||||
createChannelApprovalForwardingEvaluator,
|
||||
createChannelApproverDmTargetResolver,
|
||||
createChannelNativeOriginTargetResolver,
|
||||
createNativeApprovalForwardingFallbackSuppressor,
|
||||
doesApprovalRequestMatchChannelAccount,
|
||||
nativeApprovalTargetsMatch,
|
||||
resolveApprovalRequestSessionTarget,
|
||||
shouldSuppressLocalNativeExecApprovalPrompt,
|
||||
@@ -41,7 +40,6 @@ import { normalizeSignalMessagingTarget } from "./normalize.js";
|
||||
type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest;
|
||||
type ApprovalKind = "exec" | "plugin";
|
||||
type ApprovalForwardingConfig = NonNullable<NonNullable<OpenClawConfig["approvals"]>["exec"]>;
|
||||
type ApprovalForwardingMode = NonNullable<ApprovalForwardingConfig["mode"]>;
|
||||
type ChannelApprovalForwardTarget = Parameters<
|
||||
NonNullable<
|
||||
NonNullable<ChannelApprovalCapability["delivery"]>["shouldSuppressForwardingFallback"]
|
||||
@@ -53,8 +51,6 @@ type SignalApprovalTarget = {
|
||||
threadId?: string | number | null;
|
||||
};
|
||||
|
||||
const DEFAULT_APPROVAL_FORWARDING_MODE: ApprovalForwardingMode = "session";
|
||||
|
||||
function isSignalApprovalTransportEnabled(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
@@ -62,48 +58,6 @@ function isSignalApprovalTransportEnabled(params: {
|
||||
return resolveSignalAccount({ cfg: params.cfg, accountId: params.accountId }).enabled;
|
||||
}
|
||||
|
||||
function resolveApprovalKind(request: ApprovalRequest, approvalKind?: ApprovalKind): ApprovalKind {
|
||||
if (approvalKind) {
|
||||
return approvalKind;
|
||||
}
|
||||
return "command" in request.request ? "exec" : "plugin";
|
||||
}
|
||||
|
||||
function resolveApprovalForwardingConfig(params: {
|
||||
cfg: OpenClawConfig;
|
||||
approvalKind: ApprovalKind;
|
||||
}): ApprovalForwardingConfig | undefined {
|
||||
return params.approvalKind === "plugin"
|
||||
? params.cfg.approvals?.plugin
|
||||
: params.cfg.approvals?.exec;
|
||||
}
|
||||
|
||||
function normalizeApprovalForwardingMode(
|
||||
mode: ApprovalForwardingConfig["mode"] | undefined,
|
||||
): ApprovalForwardingMode {
|
||||
return mode ?? DEFAULT_APPROVAL_FORWARDING_MODE;
|
||||
}
|
||||
|
||||
function approvalModeIncludesSession(mode: ApprovalForwardingMode): boolean {
|
||||
return mode === "session" || mode === "both";
|
||||
}
|
||||
|
||||
function approvalModeIncludesTargets(mode: ApprovalForwardingMode): boolean {
|
||||
return mode === "targets" || mode === "both";
|
||||
}
|
||||
|
||||
function matchesForwardingFilters(params: {
|
||||
config: ApprovalForwardingConfig;
|
||||
request: ApprovalRequest;
|
||||
}): boolean {
|
||||
return matchesApprovalRequestFilters({
|
||||
request: params.request.request,
|
||||
agentFilter: params.config.agentFilter,
|
||||
sessionFilter: params.config.sessionFilter,
|
||||
fallbackAgentIdFromSessionKey: true,
|
||||
});
|
||||
}
|
||||
|
||||
function targetAccountMatchesSignalAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
targetAccountId?: string | null;
|
||||
@@ -205,52 +159,16 @@ function hasSignalOriginOrSessionTarget(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function canApprovalPotentiallyRouteToSignal(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
approvalKind: ApprovalKind;
|
||||
nativeSessionOnly?: boolean;
|
||||
}): boolean {
|
||||
if (!isSignalApprovalTransportEnabled(params)) {
|
||||
return false;
|
||||
}
|
||||
const config = resolveApprovalForwardingConfig(params);
|
||||
if (!config?.enabled) {
|
||||
return false;
|
||||
}
|
||||
const mode = normalizeApprovalForwardingMode(config.mode);
|
||||
if (approvalModeIncludesSession(mode)) {
|
||||
return true;
|
||||
}
|
||||
if (params.nativeSessionOnly) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
approvalModeIncludesTargets(mode) &&
|
||||
hasMatchingSignalTarget({
|
||||
cfg: params.cfg,
|
||||
config,
|
||||
accountId: params.accountId,
|
||||
})
|
||||
);
|
||||
}
|
||||
const signalApprovalForwarding = createChannelApprovalForwardingEvaluator({
|
||||
channel: "signal",
|
||||
isTransportEnabled: isSignalApprovalTransportEnabled,
|
||||
hasMatchingTarget: hasMatchingSignalTarget,
|
||||
hasOriginOrSessionTarget: hasSignalOriginOrSessionTarget,
|
||||
});
|
||||
|
||||
function canAnyApprovalPotentiallyRouteToSignal(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
nativeSessionOnly?: boolean;
|
||||
}): boolean {
|
||||
return (
|
||||
canApprovalPotentiallyRouteToSignal({
|
||||
...params,
|
||||
approvalKind: "exec",
|
||||
}) ||
|
||||
canApprovalPotentiallyRouteToSignal({
|
||||
...params,
|
||||
approvalKind: "plugin",
|
||||
})
|
||||
);
|
||||
}
|
||||
const canApprovalPotentiallyRouteToSignal = signalApprovalForwarding.isPotentialRoute;
|
||||
const canAnyApprovalPotentiallyRouteToSignal = signalApprovalForwarding.canAnyPotentiallyRoute;
|
||||
const isSignalSessionApprovalEligible = signalApprovalForwarding.isSessionEligible;
|
||||
|
||||
export function isSignalNativeApprovalHandlerConfigured(params: {
|
||||
cfg: OpenClawConfig;
|
||||
@@ -262,43 +180,6 @@ export function isSignalNativeApprovalHandlerConfigured(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function isSignalSessionApprovalEligible(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
approvalKind: ApprovalKind;
|
||||
request: ApprovalRequest;
|
||||
}): boolean {
|
||||
if (!isSignalApprovalTransportEnabled(params)) {
|
||||
return false;
|
||||
}
|
||||
const config = resolveApprovalForwardingConfig(params);
|
||||
if (!config?.enabled) {
|
||||
return false;
|
||||
}
|
||||
const mode = normalizeApprovalForwardingMode(config.mode);
|
||||
if (!approvalModeIncludesSession(mode)) {
|
||||
return false;
|
||||
}
|
||||
if (!matchesForwardingFilters({ config, request: params.request })) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!doesApprovalRequestMatchChannelAccount({
|
||||
cfg: params.cfg,
|
||||
request: params.request,
|
||||
channel: "signal",
|
||||
accountId: params.accountId,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return hasSignalOriginOrSessionTarget({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
request: params.request,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveTurnSourceSignalOriginTarget(
|
||||
request: ApprovalRequest,
|
||||
): SignalApprovalTarget | null {
|
||||
@@ -330,10 +211,7 @@ function shouldHandleSignalApprovalRequest(params: {
|
||||
approvalKind?: ApprovalKind;
|
||||
request: ApprovalRequest;
|
||||
}): boolean {
|
||||
return isSignalSessionApprovalEligible({
|
||||
...params,
|
||||
approvalKind: resolveApprovalKind(params.request, params.approvalKind),
|
||||
});
|
||||
return signalApprovalForwarding.shouldHandleRequest(params);
|
||||
}
|
||||
|
||||
function resolveSignalSessionTargetFromSessionKey(sessionKey?: string | null): string | null {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {
|
||||
buildChannelApprovalExpiredText,
|
||||
buildChannelApprovalResolvedText,
|
||||
createChannelApprovalNativeRuntimeAdapter,
|
||||
type ExpiredApprovalView,
|
||||
type PendingApprovalView,
|
||||
type ResolvedApprovalView,
|
||||
resolvePreparedApprovalAccountId,
|
||||
} from "openclaw/plugin-sdk/approval-handler-runtime";
|
||||
import { buildChannelApprovalNativeTargetKey } from "openclaw/plugin-sdk/approval-native-runtime";
|
||||
import {
|
||||
@@ -10,16 +11,10 @@ import {
|
||||
type ApprovalReactionPendingContent,
|
||||
} from "openclaw/plugin-sdk/approval-reaction-runtime";
|
||||
import {
|
||||
buildApprovalResolvedReplyPayload,
|
||||
buildPluginApprovalExpiredMessage,
|
||||
buildPluginApprovalResolvedMessage,
|
||||
type ExecApprovalRequest,
|
||||
type ExecApprovalResolved,
|
||||
type PluginApprovalRequest,
|
||||
type PluginApprovalResolved,
|
||||
} from "openclaw/plugin-sdk/approval-runtime";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import {
|
||||
registerWhatsAppApprovalReactionTarget,
|
||||
unregisterWhatsAppApprovalReactionTarget,
|
||||
@@ -31,7 +26,6 @@ import { sendMessageWhatsApp, sendTypingWhatsApp } from "./send.js";
|
||||
const log = createSubsystemLogger("whatsapp/approvals");
|
||||
|
||||
type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest;
|
||||
type ApprovalResolved = ExecApprovalResolved | PluginApprovalResolved;
|
||||
type WhatsAppPendingDelivery = ApprovalReactionPendingContent;
|
||||
type PreparedWhatsAppApprovalTarget = {
|
||||
to: string;
|
||||
@@ -55,42 +49,6 @@ function buildPendingPayload(params: {
|
||||
return buildApprovalReactionPendingContent(params);
|
||||
}
|
||||
|
||||
function buildResolvedText(params: {
|
||||
request: ApprovalRequest;
|
||||
resolved: ApprovalResolved;
|
||||
view: ResolvedApprovalView;
|
||||
}): string {
|
||||
if (params.view.approvalKind === "plugin") {
|
||||
return buildPluginApprovalResolvedMessage(params.resolved as PluginApprovalResolved);
|
||||
}
|
||||
const resolvedByText = params.resolved.resolvedBy
|
||||
? ` Resolved by ${params.resolved.resolvedBy}.`
|
||||
: "";
|
||||
const payload = buildApprovalResolvedReplyPayload({
|
||||
approvalId: params.request.id,
|
||||
approvalSlug: params.request.id.slice(0, 8),
|
||||
text: `✅ Exec approval ${params.resolved.decision}.${resolvedByText} ID: ${params.request.id}`,
|
||||
});
|
||||
return payload.text ?? "";
|
||||
}
|
||||
|
||||
function buildExpiredText(params: { request: ApprovalRequest; view: ExpiredApprovalView }): string {
|
||||
if (params.view.approvalKind === "plugin") {
|
||||
return buildPluginApprovalExpiredMessage(params.request as PluginApprovalRequest);
|
||||
}
|
||||
return `⏱️ Exec approval expired. ID: ${params.request.id}`;
|
||||
}
|
||||
|
||||
function resolvePreparedAccountId(params: {
|
||||
plannedAccountId?: string | null;
|
||||
contextAccountId?: string | null;
|
||||
}): string | undefined {
|
||||
return (
|
||||
normalizeOptionalString(params.plannedAccountId) ??
|
||||
normalizeOptionalString(params.contextAccountId)
|
||||
);
|
||||
}
|
||||
|
||||
export const whatsappApprovalNativeRuntime = createChannelApprovalNativeRuntimeAdapter<
|
||||
WhatsAppPendingDelivery,
|
||||
PreparedWhatsAppApprovalTarget,
|
||||
@@ -108,11 +66,11 @@ export const whatsappApprovalNativeRuntime = createChannelApprovalNativeRuntimeA
|
||||
buildPendingPayload({ request, view, nowMs }),
|
||||
buildResolvedResult: ({ request, resolved, view }) => ({
|
||||
kind: "update",
|
||||
payload: { text: buildResolvedText({ request, resolved, view }) },
|
||||
payload: { text: buildChannelApprovalResolvedText({ request, resolved, view }) },
|
||||
}),
|
||||
buildExpiredResult: ({ request, view }) => ({
|
||||
kind: "update",
|
||||
payload: { text: buildExpiredText({ request, view }) },
|
||||
payload: { text: buildChannelApprovalExpiredText({ request, view }) },
|
||||
}),
|
||||
},
|
||||
transport: {
|
||||
@@ -123,7 +81,7 @@ export const whatsappApprovalNativeRuntime = createChannelApprovalNativeRuntimeA
|
||||
}
|
||||
const prepared: PreparedWhatsAppApprovalTarget = {
|
||||
to,
|
||||
accountId: resolvePreparedAccountId({
|
||||
accountId: resolvePreparedApprovalAccountId({
|
||||
plannedAccountId: (plannedTarget.target as { accountId?: string | null }).accountId,
|
||||
contextAccountId: accountId,
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { matchesApprovalRequestFilters } from "openclaw/plugin-sdk/approval-client-runtime";
|
||||
import {
|
||||
createChannelApprovalCapability,
|
||||
splitChannelApprovalCapability,
|
||||
@@ -6,10 +5,10 @@ import {
|
||||
import { createLazyChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-adapter-runtime";
|
||||
import type { ChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime";
|
||||
import {
|
||||
createChannelApprovalForwardingEvaluator,
|
||||
createChannelApproverDmTargetResolver,
|
||||
createChannelNativeOriginTargetResolver,
|
||||
createNativeApprovalForwardingFallbackSuppressor,
|
||||
doesApprovalRequestMatchChannelAccount,
|
||||
nativeApprovalTargetsMatch,
|
||||
resolveApprovalRequestSessionTarget,
|
||||
} from "openclaw/plugin-sdk/approval-native-runtime";
|
||||
@@ -36,7 +35,6 @@ import { isWhatsAppGroupJid, normalizeWhatsAppMessagingTarget } from "./normaliz
|
||||
type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest;
|
||||
type ApprovalKind = "exec" | "plugin";
|
||||
type ApprovalForwardingConfig = NonNullable<NonNullable<OpenClawConfig["approvals"]>["exec"]>;
|
||||
type ApprovalForwardingMode = NonNullable<ApprovalForwardingConfig["mode"]>;
|
||||
type ChannelApprovalForwardTarget = Parameters<
|
||||
NonNullable<
|
||||
NonNullable<ChannelApprovalCapability["delivery"]>["shouldSuppressForwardingFallback"]
|
||||
@@ -48,8 +46,6 @@ type WhatsAppApprovalTarget = {
|
||||
threadId?: string | number | null;
|
||||
};
|
||||
|
||||
const DEFAULT_APPROVAL_FORWARDING_MODE: ApprovalForwardingMode = "session";
|
||||
|
||||
function isWhatsAppApprovalTransportEnabled(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
@@ -57,48 +53,6 @@ function isWhatsAppApprovalTransportEnabled(params: {
|
||||
return resolveWhatsAppAccount({ cfg: params.cfg, accountId: params.accountId }).enabled;
|
||||
}
|
||||
|
||||
function resolveApprovalKind(request: ApprovalRequest, approvalKind?: ApprovalKind): ApprovalKind {
|
||||
if (approvalKind) {
|
||||
return approvalKind;
|
||||
}
|
||||
return "command" in request.request ? "exec" : "plugin";
|
||||
}
|
||||
|
||||
function resolveApprovalForwardingConfig(params: {
|
||||
cfg: OpenClawConfig;
|
||||
approvalKind: ApprovalKind;
|
||||
}): ApprovalForwardingConfig | undefined {
|
||||
return params.approvalKind === "plugin"
|
||||
? params.cfg.approvals?.plugin
|
||||
: params.cfg.approvals?.exec;
|
||||
}
|
||||
|
||||
function normalizeApprovalForwardingMode(
|
||||
mode: ApprovalForwardingConfig["mode"] | undefined,
|
||||
): ApprovalForwardingMode {
|
||||
return mode ?? DEFAULT_APPROVAL_FORWARDING_MODE;
|
||||
}
|
||||
|
||||
function approvalModeIncludesSession(mode: ApprovalForwardingMode): boolean {
|
||||
return mode === "session" || mode === "both";
|
||||
}
|
||||
|
||||
function approvalModeIncludesTargets(mode: ApprovalForwardingMode): boolean {
|
||||
return mode === "targets" || mode === "both";
|
||||
}
|
||||
|
||||
function matchesForwardingFilters(params: {
|
||||
config: ApprovalForwardingConfig;
|
||||
request: ApprovalRequest;
|
||||
}): boolean {
|
||||
return matchesApprovalRequestFilters({
|
||||
request: params.request.request,
|
||||
agentFilter: params.config.agentFilter,
|
||||
sessionFilter: params.config.sessionFilter,
|
||||
fallbackAgentIdFromSessionKey: true,
|
||||
});
|
||||
}
|
||||
|
||||
function targetAccountMatchesWhatsAppAccount(params: {
|
||||
cfg: OpenClawConfig;
|
||||
targetAccountId?: string | null;
|
||||
@@ -200,118 +154,17 @@ function hasWhatsAppOriginOrSessionTarget(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function canApprovalPotentiallyRouteToWhatsApp(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
approvalKind: ApprovalKind;
|
||||
nativeSessionOnly?: boolean;
|
||||
}): boolean {
|
||||
if (!isWhatsAppApprovalTransportEnabled(params)) {
|
||||
return false;
|
||||
}
|
||||
const config = resolveApprovalForwardingConfig(params);
|
||||
if (!config?.enabled) {
|
||||
return false;
|
||||
}
|
||||
const mode = normalizeApprovalForwardingMode(config.mode);
|
||||
if (approvalModeIncludesSession(mode)) {
|
||||
return true;
|
||||
}
|
||||
if (params.nativeSessionOnly) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
approvalModeIncludesTargets(mode) &&
|
||||
hasMatchingWhatsAppTarget({
|
||||
cfg: params.cfg,
|
||||
config,
|
||||
accountId: params.accountId,
|
||||
})
|
||||
);
|
||||
}
|
||||
const whatsappApprovalForwarding = createChannelApprovalForwardingEvaluator({
|
||||
channel: "whatsapp",
|
||||
isTransportEnabled: isWhatsAppApprovalTransportEnabled,
|
||||
hasMatchingTarget: hasMatchingWhatsAppTarget,
|
||||
hasOriginOrSessionTarget: hasWhatsAppOriginOrSessionTarget,
|
||||
});
|
||||
|
||||
function canAnyApprovalPotentiallyRouteToWhatsApp(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
nativeSessionOnly?: boolean;
|
||||
}): boolean {
|
||||
return (
|
||||
canApprovalPotentiallyRouteToWhatsApp({
|
||||
...params,
|
||||
approvalKind: "exec",
|
||||
}) ||
|
||||
canApprovalPotentiallyRouteToWhatsApp({
|
||||
...params,
|
||||
approvalKind: "plugin",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function isWhatsAppSessionApprovalEligible(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
approvalKind: ApprovalKind;
|
||||
request: ApprovalRequest;
|
||||
}): boolean {
|
||||
if (!isWhatsAppApprovalTransportEnabled(params)) {
|
||||
return false;
|
||||
}
|
||||
const config = resolveApprovalForwardingConfig(params);
|
||||
if (!config?.enabled) {
|
||||
return false;
|
||||
}
|
||||
const mode = normalizeApprovalForwardingMode(config.mode);
|
||||
if (!approvalModeIncludesSession(mode)) {
|
||||
return false;
|
||||
}
|
||||
if (!matchesForwardingFilters({ config, request: params.request })) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!doesApprovalRequestMatchChannelAccount({
|
||||
cfg: params.cfg,
|
||||
request: params.request,
|
||||
channel: "whatsapp",
|
||||
accountId: params.accountId,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return hasWhatsAppOriginOrSessionTarget({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
request: params.request,
|
||||
});
|
||||
}
|
||||
|
||||
function isWhatsAppExplicitTargetEligible(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
approvalKind: ApprovalKind;
|
||||
request: ApprovalRequest;
|
||||
target: ChannelApprovalForwardTarget;
|
||||
}): boolean {
|
||||
if (!isWhatsAppApprovalTransportEnabled(params)) {
|
||||
return false;
|
||||
}
|
||||
const config = resolveApprovalForwardingConfig(params);
|
||||
if (!config?.enabled) {
|
||||
return false;
|
||||
}
|
||||
const mode = normalizeApprovalForwardingMode(config.mode);
|
||||
if (!approvalModeIncludesTargets(mode)) {
|
||||
return false;
|
||||
}
|
||||
if (!matchesForwardingFilters({ config, request: params.request })) {
|
||||
return false;
|
||||
}
|
||||
return hasMatchingWhatsAppTarget({
|
||||
cfg: params.cfg,
|
||||
config,
|
||||
accountId: params.accountId,
|
||||
target: params.target,
|
||||
});
|
||||
}
|
||||
const canApprovalPotentiallyRouteToWhatsApp = whatsappApprovalForwarding.isPotentialRoute;
|
||||
const canAnyApprovalPotentiallyRouteToWhatsApp = whatsappApprovalForwarding.canAnyPotentiallyRoute;
|
||||
const isWhatsAppSessionApprovalEligible = whatsappApprovalForwarding.isSessionEligible;
|
||||
const isWhatsAppExplicitTargetEligible = whatsappApprovalForwarding.isExplicitTargetEligible;
|
||||
|
||||
function resolveTurnSourceWhatsAppOriginTarget(
|
||||
request: ApprovalRequest,
|
||||
@@ -344,10 +197,7 @@ function shouldHandleWhatsAppApprovalRequest(params: {
|
||||
approvalKind?: ApprovalKind;
|
||||
request: ApprovalRequest;
|
||||
}): boolean {
|
||||
return isWhatsAppSessionApprovalEligible({
|
||||
...params,
|
||||
approvalKind: resolveApprovalKind(params.request, params.approvalKind),
|
||||
});
|
||||
return whatsappApprovalForwarding.shouldHandleRequest(params);
|
||||
}
|
||||
|
||||
const resolveWhatsAppOriginTargetBase = createChannelNativeOriginTargetResolver({
|
||||
|
||||
@@ -29,3 +29,70 @@ export {
|
||||
type ResolvedApprovalView,
|
||||
} from "../infra/approval-handler-runtime.js";
|
||||
export { resolveApprovalOverGateway } from "./approval-gateway-runtime.js";
|
||||
import type {
|
||||
ExpiredApprovalView,
|
||||
ResolvedApprovalView,
|
||||
} from "../infra/approval-view-model.types.js";
|
||||
import type { ExecApprovalRequest, ExecApprovalResolved } from "../infra/exec-approvals.js";
|
||||
import {
|
||||
buildPluginApprovalExpiredMessage,
|
||||
buildPluginApprovalResolvedMessage,
|
||||
type PluginApprovalRequest,
|
||||
type PluginApprovalResolved,
|
||||
} from "../infra/plugin-approvals.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { buildApprovalResolvedReplyPayload } from "./approval-renderers.js";
|
||||
|
||||
type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest;
|
||||
type ApprovalResolved = ExecApprovalResolved | PluginApprovalResolved;
|
||||
|
||||
export function buildChannelApprovalResolvedText(params: {
|
||||
request: ApprovalRequest;
|
||||
resolved: ApprovalResolved;
|
||||
view: ResolvedApprovalView;
|
||||
}): string {
|
||||
if (params.view.approvalKind === "plugin") {
|
||||
return buildPluginApprovalResolvedMessage(params.resolved as PluginApprovalResolved);
|
||||
}
|
||||
const resolvedByText = params.resolved.resolvedBy
|
||||
? ` Resolved by ${params.resolved.resolvedBy}.`
|
||||
: "";
|
||||
const payload = buildApprovalResolvedReplyPayload({
|
||||
approvalId: params.request.id,
|
||||
approvalSlug: params.request.id.slice(0, 8),
|
||||
text: `✅ Exec approval ${params.resolved.decision}.${resolvedByText} ID: ${params.request.id}`,
|
||||
});
|
||||
return payload.text ?? "";
|
||||
}
|
||||
|
||||
export function buildChannelApprovalExpiredText(params: {
|
||||
request: ApprovalRequest;
|
||||
view: ExpiredApprovalView;
|
||||
}): string {
|
||||
if (params.view.approvalKind === "plugin") {
|
||||
return buildPluginApprovalExpiredMessage(params.request as PluginApprovalRequest);
|
||||
}
|
||||
return `⏱️ Exec approval expired. ID: ${params.request.id}`;
|
||||
}
|
||||
|
||||
export function resolvePreparedApprovalAccountId(params: {
|
||||
plannedAccountId?: string | null;
|
||||
contextAccountId?: string | null;
|
||||
fallbackAccountId: string;
|
||||
}): string;
|
||||
export function resolvePreparedApprovalAccountId(params: {
|
||||
plannedAccountId?: string | null;
|
||||
contextAccountId?: string | null;
|
||||
fallbackAccountId?: string | null;
|
||||
}): string | undefined;
|
||||
export function resolvePreparedApprovalAccountId(params: {
|
||||
plannedAccountId?: string | null;
|
||||
contextAccountId?: string | null;
|
||||
fallbackAccountId?: string | null;
|
||||
}): string | undefined {
|
||||
return (
|
||||
normalizeOptionalString(params.plannedAccountId) ??
|
||||
normalizeOptionalString(params.contextAccountId) ??
|
||||
normalizeOptionalString(params.fallbackAccountId)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import type {
|
||||
ExecApprovalForwardingConfig,
|
||||
ExecApprovalForwardingMode,
|
||||
} from "../config/types.approvals.js";
|
||||
import { doesApprovalRequestMatchChannelAccount } from "../infra/approval-request-account-binding.js";
|
||||
import { matchesApprovalRequestFilters } from "../infra/approval-request-filters.js";
|
||||
import {
|
||||
getExecApprovalReplyMetadata,
|
||||
@@ -26,6 +31,7 @@ type LocalNativeExecApprovalConfig = {
|
||||
agentFilter?: string[];
|
||||
sessionFilter?: string[];
|
||||
};
|
||||
type ChannelApprovalForwardTarget = DeliverySuppressionInput["target"];
|
||||
|
||||
type ApprovalResolverParams = {
|
||||
cfg: OpenClawConfig;
|
||||
@@ -34,6 +40,41 @@ type ApprovalResolverParams = {
|
||||
request: ApprovalRequest;
|
||||
};
|
||||
|
||||
type ChannelApprovalForwardingEvaluatorParams = {
|
||||
channel: string;
|
||||
isTransportEnabled: (params: { cfg: OpenClawConfig; accountId?: string | null }) => boolean;
|
||||
hasMatchingTarget: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
config: ExecApprovalForwardingConfig;
|
||||
accountId?: string | null;
|
||||
target?: ChannelApprovalForwardTarget;
|
||||
}) => boolean;
|
||||
hasOriginOrSessionTarget: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
request: ApprovalRequest;
|
||||
}) => boolean;
|
||||
};
|
||||
|
||||
export type ChannelApprovalForwardingEligibilityParams = {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
approvalKind: ApprovalKind;
|
||||
request: ApprovalRequest;
|
||||
};
|
||||
|
||||
export type ChannelApprovalPotentialRouteParams = {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
approvalKind: ApprovalKind;
|
||||
nativeSessionOnly?: boolean;
|
||||
};
|
||||
|
||||
export type ChannelApprovalExplicitTargetEligibilityParams =
|
||||
ChannelApprovalForwardingEligibilityParams & {
|
||||
target: ChannelApprovalForwardTarget;
|
||||
};
|
||||
|
||||
type NativeApprovalTargetNormalizer<TTarget> = (
|
||||
target: TTarget,
|
||||
request: ApprovalRequest,
|
||||
@@ -230,13 +271,170 @@ function nativeApprovalTargetMatcher(channel: string): (left: unknown, right: un
|
||||
nativeApprovalTargetsMatch({ channel, left, right });
|
||||
}
|
||||
|
||||
function resolveApprovalKind(request: ApprovalRequest, approvalKind?: ApprovalKind): ApprovalKind {
|
||||
export function resolveApprovalKind(
|
||||
request: ApprovalRequest,
|
||||
approvalKind?: ApprovalKind,
|
||||
): ApprovalKind {
|
||||
if (approvalKind) {
|
||||
return approvalKind;
|
||||
}
|
||||
return "command" in request.request ? "exec" : "plugin";
|
||||
}
|
||||
|
||||
function resolveApprovalForwardingConfig(params: {
|
||||
cfg: OpenClawConfig;
|
||||
approvalKind: ApprovalKind;
|
||||
}): ExecApprovalForwardingConfig | undefined {
|
||||
return params.approvalKind === "plugin"
|
||||
? params.cfg.approvals?.plugin
|
||||
: params.cfg.approvals?.exec;
|
||||
}
|
||||
|
||||
function normalizeApprovalForwardingMode(
|
||||
mode: ExecApprovalForwardingConfig["mode"] | undefined,
|
||||
): ExecApprovalForwardingMode {
|
||||
return mode ?? "session";
|
||||
}
|
||||
|
||||
function approvalModeIncludesSession(mode: ExecApprovalForwardingMode): boolean {
|
||||
return mode === "session" || mode === "both";
|
||||
}
|
||||
|
||||
function approvalModeIncludesTargets(mode: ExecApprovalForwardingMode): boolean {
|
||||
return mode === "targets" || mode === "both";
|
||||
}
|
||||
|
||||
function matchesForwardingFilters(params: {
|
||||
config: ExecApprovalForwardingConfig;
|
||||
request: ApprovalRequest;
|
||||
}): boolean {
|
||||
return matchesApprovalRequestFilters({
|
||||
request: params.request.request,
|
||||
agentFilter: params.config.agentFilter,
|
||||
sessionFilter: params.config.sessionFilter,
|
||||
fallbackAgentIdFromSessionKey: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function createChannelApprovalForwardingEvaluator(
|
||||
params: ChannelApprovalForwardingEvaluatorParams,
|
||||
) {
|
||||
const isPotentialRoute = (input: ChannelApprovalPotentialRouteParams): boolean => {
|
||||
if (!params.isTransportEnabled(input)) {
|
||||
return false;
|
||||
}
|
||||
const config = resolveApprovalForwardingConfig(input);
|
||||
if (!config?.enabled) {
|
||||
return false;
|
||||
}
|
||||
const mode = normalizeApprovalForwardingMode(config.mode);
|
||||
if (approvalModeIncludesSession(mode)) {
|
||||
return true;
|
||||
}
|
||||
if (input.nativeSessionOnly) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
approvalModeIncludesTargets(mode) &&
|
||||
params.hasMatchingTarget({
|
||||
cfg: input.cfg,
|
||||
config,
|
||||
accountId: input.accountId,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const isSessionEligible = (input: ChannelApprovalForwardingEligibilityParams): boolean => {
|
||||
if (!params.isTransportEnabled(input)) {
|
||||
return false;
|
||||
}
|
||||
const config = resolveApprovalForwardingConfig(input);
|
||||
if (!config?.enabled) {
|
||||
return false;
|
||||
}
|
||||
const mode = normalizeApprovalForwardingMode(config.mode);
|
||||
if (!approvalModeIncludesSession(mode)) {
|
||||
return false;
|
||||
}
|
||||
if (!matchesForwardingFilters({ config, request: input.request })) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!doesApprovalRequestMatchChannelAccount({
|
||||
cfg: input.cfg,
|
||||
request: input.request,
|
||||
channel: params.channel,
|
||||
accountId: input.accountId,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return params.hasOriginOrSessionTarget({
|
||||
cfg: input.cfg,
|
||||
accountId: input.accountId,
|
||||
request: input.request,
|
||||
});
|
||||
};
|
||||
|
||||
const isExplicitTargetEligible = (
|
||||
input: ChannelApprovalExplicitTargetEligibilityParams,
|
||||
): boolean => {
|
||||
if (!params.isTransportEnabled(input)) {
|
||||
return false;
|
||||
}
|
||||
const config = resolveApprovalForwardingConfig(input);
|
||||
if (!config?.enabled) {
|
||||
return false;
|
||||
}
|
||||
const mode = normalizeApprovalForwardingMode(config.mode);
|
||||
if (!approvalModeIncludesTargets(mode)) {
|
||||
return false;
|
||||
}
|
||||
if (!matchesForwardingFilters({ config, request: input.request })) {
|
||||
return false;
|
||||
}
|
||||
return params.hasMatchingTarget({
|
||||
cfg: input.cfg,
|
||||
config,
|
||||
accountId: input.accountId,
|
||||
target: input.target,
|
||||
});
|
||||
};
|
||||
|
||||
const canAnyPotentiallyRoute = (input: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
nativeSessionOnly?: boolean;
|
||||
}): boolean =>
|
||||
isPotentialRoute({
|
||||
...input,
|
||||
approvalKind: "exec",
|
||||
}) ||
|
||||
isPotentialRoute({
|
||||
...input,
|
||||
approvalKind: "plugin",
|
||||
});
|
||||
|
||||
const shouldHandleRequest = (input: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
approvalKind?: ApprovalKind;
|
||||
request: ApprovalRequest;
|
||||
}): boolean =>
|
||||
isSessionEligible({
|
||||
...input,
|
||||
approvalKind: resolveApprovalKind(input.request, input.approvalKind),
|
||||
});
|
||||
|
||||
return {
|
||||
canAnyPotentiallyRoute,
|
||||
isExplicitTargetEligible,
|
||||
isPotentialRoute,
|
||||
isSessionEligible,
|
||||
shouldHandleRequest,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeOptionalAccountId(value?: string | null): string | undefined {
|
||||
return value?.trim() || undefined;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
export {
|
||||
createChannelApprovalForwardingEvaluator,
|
||||
createChannelApproverDmTargetResolver,
|
||||
createChannelNativeOriginTargetResolver,
|
||||
createNativeApprovalForwardingFallbackSuppressor,
|
||||
nativeApprovalTargetsMatch,
|
||||
resolveApprovalKind,
|
||||
shouldSuppressLocalNativeExecApprovalPrompt,
|
||||
type ChannelApprovalExplicitTargetEligibilityParams,
|
||||
type ChannelApprovalForwardingEligibilityParams,
|
||||
type ChannelApprovalPotentialRouteParams,
|
||||
} from "./approval-native-helpers.js";
|
||||
export {
|
||||
resolveApprovalRequestSessionConversation,
|
||||
|
||||
Reference in New Issue
Block a user