mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 03:33:52 +00:00
Expose plugin approval action metadata so plugins can describe richer approval actions across gateway, SDK, channel, and UI surfaces.
232 lines
7.9 KiB
TypeScript
232 lines
7.9 KiB
TypeScript
import type {
|
|
ChannelApprovalCapabilityHandlerContext,
|
|
PendingApprovalView,
|
|
} from "openclaw/plugin-sdk/approval-handler-runtime";
|
|
import { createChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime";
|
|
import { buildChannelApprovalNativeTargetKey } from "openclaw/plugin-sdk/approval-native-runtime";
|
|
import { buildPluginApprovalPendingReplyPayload } from "openclaw/plugin-sdk/approval-reply-runtime";
|
|
import {
|
|
buildApprovalPresentationFromActionDescriptors,
|
|
buildExecApprovalPendingReplyPayload,
|
|
} from "openclaw/plugin-sdk/approval-reply-runtime";
|
|
import type {
|
|
ExecApprovalActionDescriptor,
|
|
ExecApprovalPendingReplyParams,
|
|
ExecApprovalReplyDecision,
|
|
} from "openclaw/plugin-sdk/approval-reply-runtime";
|
|
import type {
|
|
ExecApprovalRequest,
|
|
PluginApprovalRequest,
|
|
} from "openclaw/plugin-sdk/approval-runtime";
|
|
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
|
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
import { resolveTelegramInlineButtons } from "./button-types.js";
|
|
import {
|
|
isTelegramExecApprovalHandlerConfigured,
|
|
shouldHandleTelegramExecApprovalRequest,
|
|
} from "./exec-approvals.js";
|
|
import { editMessageReplyMarkupTelegram, sendMessageTelegram, sendTypingTelegram } from "./send.js";
|
|
|
|
const log = createSubsystemLogger("telegram/approvals");
|
|
|
|
type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest;
|
|
type PendingMessage = {
|
|
chatId: string;
|
|
messageId: string;
|
|
};
|
|
type TelegramPendingDelivery = {
|
|
text: string;
|
|
buttons: ReturnType<typeof resolveTelegramInlineButtons>;
|
|
};
|
|
|
|
export type TelegramExecApprovalHandlerDeps = {
|
|
nowMs?: () => number;
|
|
sendTyping?: typeof sendTypingTelegram;
|
|
sendMessage?: typeof sendMessageTelegram;
|
|
editReplyMarkup?: typeof editMessageReplyMarkupTelegram;
|
|
};
|
|
|
|
export type TelegramApprovalHandlerContext = {
|
|
token: string;
|
|
deps?: TelegramExecApprovalHandlerDeps;
|
|
};
|
|
|
|
function resolveHandlerContext(params: ChannelApprovalCapabilityHandlerContext): {
|
|
accountId: string;
|
|
context: TelegramApprovalHandlerContext;
|
|
} | null {
|
|
const context = params.context as TelegramApprovalHandlerContext | undefined;
|
|
const accountId = normalizeOptionalString(params.accountId) ?? "";
|
|
if (!context?.token || !accountId) {
|
|
return null;
|
|
}
|
|
return { accountId, context };
|
|
}
|
|
|
|
function listDecisionActions(view: PendingApprovalView): ExecApprovalReplyDecision[] {
|
|
return view.actions.flatMap((action) => (action.kind === "decision" ? [action.decision] : []));
|
|
}
|
|
|
|
function buildTelegramApprovalCommandText(params: {
|
|
approvalCommandId: string;
|
|
decision: ExecApprovalReplyDecision;
|
|
}): string {
|
|
return `/approve ${params.approvalCommandId} ${params.decision}`;
|
|
}
|
|
|
|
function listNativeButtonActions(view: PendingApprovalView): ExecApprovalActionDescriptor[] {
|
|
return view.actions.flatMap((action) =>
|
|
action.kind === "decision"
|
|
? [
|
|
{
|
|
kind: "decision",
|
|
decision: action.decision,
|
|
label: action.label,
|
|
style: action.style,
|
|
command: buildTelegramApprovalCommandText({
|
|
approvalCommandId: view.approvalId,
|
|
decision: action.decision,
|
|
}),
|
|
},
|
|
]
|
|
: [],
|
|
);
|
|
}
|
|
|
|
function buildPendingPayload(params: {
|
|
request: ApprovalRequest;
|
|
approvalKind: "exec" | "plugin";
|
|
nowMs: number;
|
|
view: PendingApprovalView;
|
|
}): TelegramPendingDelivery {
|
|
const payload =
|
|
params.approvalKind === "plugin"
|
|
? buildPluginApprovalPendingReplyPayload({
|
|
request: params.request as PluginApprovalRequest,
|
|
nowMs: params.nowMs,
|
|
})
|
|
: buildExecApprovalPendingReplyPayload({
|
|
approvalId: params.request.id,
|
|
approvalSlug: params.request.id.slice(0, 8),
|
|
approvalCommandId: params.request.id,
|
|
warningText:
|
|
params.view.approvalKind === "exec"
|
|
? (params.view.warningText ?? undefined)
|
|
: undefined,
|
|
command: params.view.approvalKind === "exec" ? params.view.commandText : "",
|
|
cwd: params.view.approvalKind === "exec" ? (params.view.cwd ?? undefined) : undefined,
|
|
host:
|
|
params.view.approvalKind === "exec" && params.view.host === "node" ? "node" : "gateway",
|
|
nodeId:
|
|
params.view.approvalKind === "exec" ? (params.view.nodeId ?? undefined) : undefined,
|
|
allowedDecisions: listDecisionActions(params.view),
|
|
expiresAtMs: params.request.expiresAtMs,
|
|
nowMs: params.nowMs,
|
|
} satisfies ExecApprovalPendingReplyParams);
|
|
return {
|
|
text: payload.text ?? "",
|
|
buttons: resolveTelegramInlineButtons({
|
|
presentation: buildApprovalPresentationFromActionDescriptors(
|
|
listNativeButtonActions(params.view),
|
|
),
|
|
}),
|
|
};
|
|
}
|
|
|
|
export const telegramApprovalNativeRuntime = createChannelApprovalNativeRuntimeAdapter<
|
|
TelegramPendingDelivery,
|
|
{ chatId: string; messageThreadId?: number },
|
|
PendingMessage,
|
|
never
|
|
>({
|
|
eventKinds: ["exec", "plugin"],
|
|
availability: {
|
|
isConfigured: (params) => {
|
|
const resolved = resolveHandlerContext(params);
|
|
return resolved
|
|
? isTelegramExecApprovalHandlerConfigured({
|
|
cfg: params.cfg,
|
|
accountId: resolved.accountId,
|
|
})
|
|
: false;
|
|
},
|
|
shouldHandle: (params) => {
|
|
const resolved = resolveHandlerContext(params);
|
|
return resolved
|
|
? shouldHandleTelegramExecApprovalRequest({
|
|
cfg: params.cfg,
|
|
accountId: resolved.accountId,
|
|
request: params.request,
|
|
})
|
|
: false;
|
|
},
|
|
},
|
|
presentation: {
|
|
buildPendingPayload: ({ request, approvalKind, nowMs, view }) =>
|
|
buildPendingPayload({ request, approvalKind, nowMs, view }),
|
|
buildResolvedResult: () => ({ kind: "clear-actions" }),
|
|
buildExpiredResult: () => ({ kind: "clear-actions" }),
|
|
},
|
|
transport: {
|
|
prepareTarget: ({ plannedTarget }) => ({
|
|
dedupeKey: buildChannelApprovalNativeTargetKey(plannedTarget.target),
|
|
target: {
|
|
chatId: plannedTarget.target.to,
|
|
messageThreadId:
|
|
typeof plannedTarget.target.threadId === "number"
|
|
? plannedTarget.target.threadId
|
|
: undefined,
|
|
},
|
|
}),
|
|
deliverPending: async ({ cfg, accountId, context, preparedTarget, pendingPayload }) => {
|
|
const resolved = resolveHandlerContext({ cfg, accountId, context });
|
|
if (!resolved) {
|
|
return null;
|
|
}
|
|
const sendTyping = resolved.context.deps?.sendTyping ?? sendTypingTelegram;
|
|
const sendMessage = resolved.context.deps?.sendMessage ?? sendMessageTelegram;
|
|
await sendTyping(preparedTarget.chatId, {
|
|
cfg,
|
|
token: resolved.context.token,
|
|
accountId: resolved.accountId,
|
|
...(preparedTarget.messageThreadId != null
|
|
? { messageThreadId: preparedTarget.messageThreadId }
|
|
: {}),
|
|
}).catch(() => {});
|
|
const result = await sendMessage(preparedTarget.chatId, pendingPayload.text, {
|
|
cfg,
|
|
token: resolved.context.token,
|
|
accountId: resolved.accountId,
|
|
buttons: pendingPayload.buttons,
|
|
...(preparedTarget.messageThreadId != null
|
|
? { messageThreadId: preparedTarget.messageThreadId }
|
|
: {}),
|
|
});
|
|
return {
|
|
chatId: result.chatId,
|
|
messageId: result.messageId,
|
|
};
|
|
},
|
|
},
|
|
interactions: {
|
|
clearPendingActions: async ({ cfg, accountId, context, entry }) => {
|
|
const resolved = resolveHandlerContext({ cfg, accountId, context });
|
|
if (!resolved) {
|
|
return;
|
|
}
|
|
const editReplyMarkup =
|
|
resolved.context.deps?.editReplyMarkup ?? editMessageReplyMarkupTelegram;
|
|
await editReplyMarkup(entry.chatId, entry.messageId, [], {
|
|
cfg,
|
|
token: resolved.context.token,
|
|
accountId: resolved.accountId,
|
|
});
|
|
},
|
|
},
|
|
observe: {
|
|
onDeliveryError: ({ error, request }) => {
|
|
log.error(`telegram approvals: failed to send request ${request.id}: ${String(error)}`);
|
|
},
|
|
},
|
|
});
|