UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

237 lines (236 loc) 9.64 kB
import { o as callGateway } from "./call-BWR5pPgw.js"; import { _ as readAskUserQuestionId } from "./reply-payload-DahKk-7n.js"; import { v as renderMessagePresentationFallbackText } from "./payload-DYE7bQ2k.js"; import { r as registerQuestionChannelDelivery } from "./question-channel-runtime-Ds-274X0.js"; //#region src/infra/question-gateway-resolver.ts const QUESTION_RECORD_ID_PATTERN = /^ask_[a-f0-9]{32}$/u; function readTerminalReason(error) { if (!(error instanceof Error) || error.name !== "GatewayClientRequestError") return; const details = error.details; if (!details || typeof details !== "object" || Array.isArray(details)) return; const reason = details.reason; if (reason === "QUESTION_ALREADY_TERMINAL") return "already-terminal"; return reason === "QUESTION_NOT_FOUND" ? "not-found" : void 0; } /** Resolves one rendered choice or validates a custom-input transition. */ async function resolveQuestionOverGateway(params) { if (!QUESTION_RECORD_ID_PATTERN.test(params.questionId)) throw new Error("question resolution requires a valid question record id"); if (params.customInput !== true && params.optionValue === void 0 && !Number.isInteger(params.optionIndex)) throw new Error("question resolution requires an option value or index"); if (params.optionValue !== void 0 && !params.optionValue) throw new Error("question resolution requires a non-empty option value"); const gatewayOptions = { config: params.cfg, url: params.gatewayUrl, scopes: ["operator.questions"], clientDisplayName: params.clientDisplayName ?? `Question (${params.senderId?.trim() || "unknown"})` }; let getResult; try { getResult = await callGateway({ ...gatewayOptions, method: "question.get", params: { id: params.questionId } }); } catch (error) { const reason = readTerminalReason(error); if (reason) return { status: "already-terminal", reason }; throw error; } const record = getResult.question; if (record.status !== "pending") return { status: "already-terminal", reason: "already-terminal" }; const question = record.questions.length === 1 ? record.questions[0] : void 0; if (!question || question.multiSelect || question.isSecret) throw new Error("question button resolution requires one tappable question"); if (params.customInput === true) { if (!question.isOther) throw new Error("question does not allow a custom answer"); return { status: "custom-input", questionId: question.questionId }; } const optionValue = params.optionValue ?? question.options[params.optionIndex]?.label; if (!optionValue) throw new Error("question resolution index does not match a declared option"); try { await callGateway({ ...gatewayOptions, method: "question.resolve", params: { id: params.questionId, answers: { answers: { [question.questionId]: [optionValue] } }, resolvedBy: params.senderId?.trim() || void 0 } }); } catch (error) { const reason = readTerminalReason(error); if (reason) return { status: "already-terminal", reason }; throw error; } return { status: "answered", questionId: question.questionId, optionValue }; } //#endregion //#region src/infra/question-reaction-runtime.ts const QUESTION_REACTION_CHANNEL_DATA_KEY = "openclawQuestionReaction"; const QUESTION_REACTION_EMOJIS = [ "1️⃣", "2️⃣", "3️⃣", "4️⃣" ]; function readQuestionReactionBinding(payload) { const raw = payload.channelData?.[QUESTION_REACTION_CHANNEL_DATA_KEY]; if (!raw || typeof raw !== "object" || Array.isArray(raw)) return; const questionId = raw.questionId; const optionValues = raw.optionValues; return typeof questionId === "string" && questionId.length > 0 && Array.isArray(optionValues) && optionValues.length >= 1 && optionValues.length <= QUESTION_REACTION_EMOJIS.length && optionValues.every((value) => typeof value === "string" && value.length > 0) ? { questionId, optionValues: [...optionValues] } : void 0; } function resolveQuestionReactionIndex(reaction) { const index = QUESTION_REACTION_EMOJIS.indexOf(reaction); return index >= 0 ? index : void 0; } function prepareQuestionReactionPayloadForDelivery(params) { const questionId = readAskUserQuestionId(params.payload); const presentation = params.presentation ?? params.payload.presentation; if (!questionId || !presentation) return null; const buttonBlocks = presentation.blocks.filter((block) => block.type === "buttons"); const buttonBlock = buttonBlocks[0]; if (!buttonBlock || buttonBlocks.length !== 1) return null; const optionButtons = buttonBlock.buttons.filter((button) => button.action?.type === "question" && !("intent" in button.action)); if (optionButtons.length < 1 || optionButtons.length > 4) return null; const labels = []; const optionValues = []; for (const button of optionButtons) { if (button.action?.type !== "question" || button.action.questionId !== questionId || !("optionValue" in button.action) || !button.action.optionValue) return null; labels.push(button.label); optionValues.push(button.action.optionValue); } const questionBlock = presentation.blocks.find((block) => block.type === "text"); const textPresentation = { ...presentation, blocks: questionBlock ? [questionBlock] : [] }; const prompt = renderMessagePresentationFallbackText({ presentation: textPresentation }); const reactionHint = labels.map((label, index) => `${QUESTION_REACTION_EMOJIS[index]} ${label}`).join("\n"); const customInputHint = buttonBlock.buttons.some((button) => { const action = button.action; return action?.type === "question" && "intent" in action && action.intent === "custom-input"; }) ? "\n\nOr reply with your own answer." : ""; return { ...params.payload, text: `${prompt}\n\nReact with:\n${reactionHint}${customInputHint}`, presentation: void 0, presentationTextMode: void 0, channelData: { ...params.payload.channelData, [QUESTION_REACTION_CHANNEL_DATA_KEY]: { questionId, optionValues } } }; } async function resolveQuestionReactionOverGateway(params) { return await resolveQuestionOverGateway(params); } //#endregion //#region src/plugin-sdk/question-gateway-runtime.ts /** Runtime SDK subpath for Gateway-backed ask_user question controls. */ const DEFAULT_QUESTION_REACTION_TARGET_TTL_MS = 864e5; /** Creates one channel-owned target store for numbered ask_user reactions. */ function createQuestionReactionTargetStore(params) { const ttlMs = params.ttlMs ?? DEFAULT_QUESTION_REACTION_TARGET_TTL_MS; const registerChannelDelivery = params.registerChannelDelivery ?? registerQuestionChannelDelivery; const resolveReaction = params.resolveReaction ?? resolveQuestionReactionOverGateway; const targets = /* @__PURE__ */ new Map(); const findTarget = (identities) => { for (const identity of identities) { const key = params.buildKey(identity); const target = key ? targets.get(key) : void 0; if (target) return target; } }; return { register(binding, identity, metadata) { const key = params.buildKey(identity); if (!key) return false; const existing = targets.get(key); if (existing) clearTimeout(existing.cleanupTimer); const target = { ...binding, metadata, terminal: false, expiresAtMs: Date.now() + ttlMs, cleanupTimer: setTimeout(() => { if (targets.get(key) === target) targets.delete(key); }, ttlMs) }; target.cleanupTimer.unref?.(); targets.set(key, target); registerChannelDelivery({ questionId: binding.questionId, deliveryId: `${params.channel}-reaction:${key}`, finalize: () => { target.terminal = true; } }); return true; }, has(identities) { return Boolean(findTarget(identities)); }, async resolve(resolveParams) { const target = findTarget(resolveParams.identities); if (!target || params.identityMatches && !params.identityMatches(target.metadata, resolveParams.metadata)) return false; if (target.expiresAtMs <= Date.now() || target.terminal) { target.terminal = true; resolveParams.logDebug?.(`${params.channel}: stale question reaction ignored id=${target.questionId}`); return true; } const optionValue = target.optionValues[resolveParams.optionIndex]; if (!optionValue) { resolveParams.logDebug?.(`${params.channel}: out-of-range question reaction ignored id=${target.questionId}`); return true; } try { const result = await resolveReaction({ cfg: resolveParams.cfg, questionId: target.questionId, optionValue, senderId: resolveParams.senderId, gatewayUrl: resolveParams.gatewayUrl, clientDisplayName: `${params.channelDisplayName} question (${resolveParams.senderId})` }); target.terminal = result?.status === "answered" || result?.status === "already-terminal"; if (result?.status === "already-terminal") resolveParams.logDebug?.(`${params.channel}: stale question reaction ignored id=${target.questionId}`); } catch (error) { resolveParams.logDebug?.(`${params.channel}: question reaction failed id=${target.questionId}: ${String(error)}`); } return true; } }; } const questionGatewayRuntime = { resolveOption: resolveQuestionOverGateway, reactionEmojis: QUESTION_REACTION_EMOJIS, prepareReactionPayloadForDelivery: prepareQuestionReactionPayloadForDelivery, readAskUserQuestionId, readReactionBinding: readQuestionReactionBinding, resolveReactionIndex: resolveQuestionReactionIndex, resolveReaction: resolveQuestionReactionOverGateway, registerChannelDelivery: registerQuestionChannelDelivery }; //#endregion export { questionGatewayRuntime as n, createQuestionReactionTargetStore as t };