mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-23 15:11:42 +00:00
Interactive: add shared payload model
This commit is contained in:
committed by
Peter Steinberger
parent
082383b40d
commit
df2a6b1672
153
src/interactive/payload.ts
Normal file
153
src/interactive/payload.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
export type InteractiveButtonStyle = "primary" | "secondary" | "success" | "danger";
|
||||
|
||||
export type InteractiveReplyButton = {
|
||||
label: string;
|
||||
value: string;
|
||||
style?: InteractiveButtonStyle;
|
||||
};
|
||||
|
||||
export type InteractiveReplyOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type InteractiveReplyTextBlock = {
|
||||
type: "text";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type InteractiveReplyButtonsBlock = {
|
||||
type: "buttons";
|
||||
buttons: InteractiveReplyButton[];
|
||||
};
|
||||
|
||||
export type InteractiveReplySelectBlock = {
|
||||
type: "select";
|
||||
placeholder?: string;
|
||||
options: InteractiveReplyOption[];
|
||||
};
|
||||
|
||||
export type InteractiveReplyBlock =
|
||||
| InteractiveReplyTextBlock
|
||||
| InteractiveReplyButtonsBlock
|
||||
| InteractiveReplySelectBlock;
|
||||
|
||||
export type InteractiveReply = {
|
||||
blocks: InteractiveReplyBlock[];
|
||||
};
|
||||
|
||||
function readTrimmedString(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function normalizeButtonStyle(value: unknown): InteractiveButtonStyle | undefined {
|
||||
const style = readTrimmedString(value)?.toLowerCase();
|
||||
return style === "primary" || style === "secondary" || style === "success" || style === "danger"
|
||||
? style
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function normalizeInteractiveButton(raw: unknown): InteractiveReplyButton | undefined {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
const record = raw as Record<string, unknown>;
|
||||
const label = readTrimmedString(record.label) ?? readTrimmedString(record.text);
|
||||
const value =
|
||||
readTrimmedString(record.value) ??
|
||||
readTrimmedString(record.callbackData) ??
|
||||
readTrimmedString(record.callback_data);
|
||||
if (!label || !value) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
label,
|
||||
value,
|
||||
style: normalizeButtonStyle(record.style),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeInteractiveOption(raw: unknown): InteractiveReplyOption | undefined {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
const record = raw as Record<string, unknown>;
|
||||
const label = readTrimmedString(record.label) ?? readTrimmedString(record.text);
|
||||
const value = readTrimmedString(record.value);
|
||||
if (!label || !value) {
|
||||
return undefined;
|
||||
}
|
||||
return { label, value };
|
||||
}
|
||||
|
||||
function normalizeInteractiveBlock(raw: unknown): InteractiveReplyBlock | undefined {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
const record = raw as Record<string, unknown>;
|
||||
const type = readTrimmedString(record.type)?.toLowerCase();
|
||||
if (type === "text") {
|
||||
const text = readTrimmedString(record.text);
|
||||
return text ? { type: "text", text } : undefined;
|
||||
}
|
||||
if (type === "buttons") {
|
||||
const buttons = Array.isArray(record.buttons)
|
||||
? record.buttons
|
||||
.map((entry) => normalizeInteractiveButton(entry))
|
||||
.filter((entry): entry is InteractiveReplyButton => Boolean(entry))
|
||||
: [];
|
||||
return buttons.length > 0 ? { type: "buttons", buttons } : undefined;
|
||||
}
|
||||
if (type === "select") {
|
||||
const options = Array.isArray(record.options)
|
||||
? record.options
|
||||
.map((entry) => normalizeInteractiveOption(entry))
|
||||
.filter((entry): entry is InteractiveReplyOption => Boolean(entry))
|
||||
: [];
|
||||
return options.length > 0
|
||||
? {
|
||||
type: "select",
|
||||
placeholder: readTrimmedString(record.placeholder),
|
||||
options,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function normalizeInteractiveReply(raw: unknown): InteractiveReply | undefined {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
const record = raw as Record<string, unknown>;
|
||||
const blocks = Array.isArray(record.blocks)
|
||||
? record.blocks
|
||||
.map((entry) => normalizeInteractiveBlock(entry))
|
||||
.filter((entry): entry is InteractiveReplyBlock => Boolean(entry))
|
||||
: [];
|
||||
return blocks.length > 0 ? { blocks } : undefined;
|
||||
}
|
||||
|
||||
export function hasInteractiveReplyBlocks(value: unknown): value is InteractiveReply {
|
||||
return Boolean(normalizeInteractiveReply(value));
|
||||
}
|
||||
|
||||
export function resolveInteractiveTextFallback(params: {
|
||||
text?: string;
|
||||
interactive?: InteractiveReply;
|
||||
}): string | undefined {
|
||||
const text = readTrimmedString(params.text);
|
||||
if (text) {
|
||||
return params.text;
|
||||
}
|
||||
const interactiveText = (params.interactive?.blocks ?? [])
|
||||
.filter((block): block is InteractiveReplyTextBlock => block.type === "text")
|
||||
.map((block) => block.text.trim())
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
return interactiveText || params.text;
|
||||
}
|
||||
Reference in New Issue
Block a user