Files
openclaw/src/infra/exec-approval-session-target.ts
2026-05-01 23:37:55 +01:00

212 lines
6.9 KiB
TypeScript

import { resolveSessionConversationRef } from "../channels/plugins/session-conversation.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { normalizeMessageChannel } from "../utils/message-channel.js";
import {
doesApprovalRequestMatchChannelAccount,
resolvePersistedApprovalRequestSessionEntry,
} from "./approval-request-account-binding.js";
import type { ExecApprovalRequest } from "./exec-approvals.js";
import { resolveSessionDeliveryTarget } from "./outbound/targets.js";
import type { PluginApprovalRequest } from "./plugin-approvals.js";
export type ExecApprovalSessionTarget = {
channel?: string;
to: string;
accountId?: string;
threadId?: string | number;
};
export type ApprovalRequestSessionConversation = {
channel: string;
kind: "group" | "channel";
id: string;
rawId: string;
threadId?: string;
baseSessionKey: string;
baseConversationId: string;
parentConversationCandidates: string[];
};
type ApprovalRequestLike = ExecApprovalRequest | PluginApprovalRequest;
type ApprovalRequestOriginTargetResolver<TTarget> = {
cfg: OpenClawConfig;
request: ApprovalRequestLike;
channel: string;
accountId?: string | null;
resolveTurnSourceTarget: (request: ApprovalRequestLike) => TTarget | null;
resolveSessionTarget: (sessionTarget: ExecApprovalSessionTarget) => TTarget | null;
targetsMatch: (a: TTarget, b: TTarget) => boolean;
resolveFallbackTarget?: (request: ApprovalRequestLike) => TTarget | null;
};
function normalizeOptionalThreadValue(value?: string | number | null): string | number | undefined {
if (typeof value === "number") {
return Number.isFinite(value) ? value : undefined;
}
if (typeof value !== "string") {
return undefined;
}
const normalized = value.trim();
return normalized ? normalized : undefined;
}
function isExecApprovalRequest(request: ApprovalRequestLike): request is ExecApprovalRequest {
return "command" in request.request;
}
function toExecLikeApprovalRequest(request: ApprovalRequestLike): ExecApprovalRequest {
if (isExecApprovalRequest(request)) {
return request;
}
return {
id: request.id,
request: {
command: request.request.title,
sessionKey: request.request.sessionKey ?? undefined,
turnSourceChannel: request.request.turnSourceChannel ?? undefined,
turnSourceTo: request.request.turnSourceTo ?? undefined,
turnSourceAccountId: request.request.turnSourceAccountId ?? undefined,
turnSourceThreadId: request.request.turnSourceThreadId ?? undefined,
},
createdAtMs: request.createdAtMs,
expiresAtMs: request.expiresAtMs,
};
}
function normalizeOptionalChannel(value?: string | null): string | undefined {
return normalizeMessageChannel(value);
}
export function resolveApprovalRequestSessionConversation(params: {
request: ApprovalRequestLike;
channel?: string | null;
bundledFallback?: boolean;
}): ApprovalRequestSessionConversation | null {
const sessionKey = normalizeOptionalString(params.request.request.sessionKey);
if (!sessionKey) {
return null;
}
const resolved = resolveSessionConversationRef(sessionKey, {
bundledFallback: params.bundledFallback,
});
if (!resolved) {
return null;
}
const expectedChannel = normalizeOptionalChannel(params.channel);
if (expectedChannel && normalizeOptionalChannel(resolved.channel) !== expectedChannel) {
return null;
}
return {
channel: resolved.channel,
kind: resolved.kind,
id: resolved.id,
rawId: resolved.rawId,
threadId: resolved.threadId,
baseSessionKey: resolved.baseSessionKey,
baseConversationId: resolved.baseConversationId,
parentConversationCandidates: resolved.parentConversationCandidates,
};
}
export function resolveExecApprovalSessionTarget(params: {
cfg: OpenClawConfig;
request: ExecApprovalRequest;
turnSourceChannel?: string | null;
turnSourceTo?: string | null;
turnSourceAccountId?: string | null;
turnSourceThreadId?: string | number | null;
}): ExecApprovalSessionTarget | null {
const sessionKey = normalizeOptionalString(params.request.request.sessionKey);
if (!sessionKey) {
return null;
}
const persisted = resolvePersistedApprovalRequestSessionEntry({
cfg: params.cfg,
request: params.request,
});
if (!persisted) {
return null;
}
const target = resolveSessionDeliveryTarget({
entry: persisted.entry,
requestedChannel: "last",
turnSourceChannel: normalizeOptionalString(params.turnSourceChannel),
turnSourceTo: normalizeOptionalString(params.turnSourceTo),
turnSourceAccountId: normalizeOptionalString(params.turnSourceAccountId),
turnSourceThreadId: normalizeOptionalThreadValue(params.turnSourceThreadId),
});
if (!target.to) {
return null;
}
return {
channel: normalizeOptionalString(target.channel),
to: target.to,
accountId: normalizeOptionalString(target.accountId),
threadId: normalizeOptionalThreadValue(target.threadId),
};
}
export function resolveApprovalRequestSessionTarget(params: {
cfg: OpenClawConfig;
request: ApprovalRequestLike;
}): ExecApprovalSessionTarget | null {
const execLikeRequest = toExecLikeApprovalRequest(params.request);
return resolveExecApprovalSessionTarget({
cfg: params.cfg,
request: execLikeRequest,
turnSourceChannel: execLikeRequest.request.turnSourceChannel ?? undefined,
turnSourceTo: execLikeRequest.request.turnSourceTo ?? undefined,
turnSourceAccountId: execLikeRequest.request.turnSourceAccountId ?? undefined,
turnSourceThreadId: execLikeRequest.request.turnSourceThreadId ?? undefined,
});
}
function resolveApprovalRequestStoredSessionTarget(params: {
cfg: OpenClawConfig;
request: ApprovalRequestLike;
}): ExecApprovalSessionTarget | null {
const execLikeRequest = toExecLikeApprovalRequest(params.request);
return resolveExecApprovalSessionTarget({
cfg: params.cfg,
request: execLikeRequest,
});
}
export function resolveApprovalRequestOriginTarget<TTarget>(
params: ApprovalRequestOriginTargetResolver<TTarget>,
): TTarget | null {
if (
!doesApprovalRequestMatchChannelAccount({
cfg: params.cfg,
request: params.request,
channel: params.channel,
accountId: params.accountId,
})
) {
return null;
}
const turnSourceTarget = params.resolveTurnSourceTarget(params.request);
const expectedChannel = normalizeOptionalChannel(params.channel);
const sessionTargetBinding = resolveApprovalRequestStoredSessionTarget({
cfg: params.cfg,
request: params.request,
});
const sessionTarget =
sessionTargetBinding &&
normalizeOptionalChannel(sessionTargetBinding.channel) === expectedChannel
? params.resolveSessionTarget(sessionTargetBinding)
: null;
if (turnSourceTarget && sessionTarget && !params.targetsMatch(turnSourceTarget, sessionTarget)) {
return null;
}
return (
turnSourceTarget ?? sessionTarget ?? params.resolveFallbackTarget?.(params.request) ?? null
);
}