mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(feishu): preserve button command values in fallback text and add Feishu comment guidance with callback privacy (#94385)
* fix(interactive): preserve button command values in fallback text for degraded approval UX * fix(interactive): keep callback values private in fallback text and narrow Feishu interactive detection - P1: Skip rendering action.type === "callback" values in renderMessagePresentationFallbackText to avoid leaking opaque channel/plugin data into user-visible text. Command and legacy values are still rendered. - P2: Replace hasMessagePresentationBlocks/hasInteractiveReplyBlocks with isMessagePresentationInteractiveBlock so Feishu comment guidance only appears when the presentation actually contains buttons or selects, not for text-only blocks. - Update tests: callback button now shows label-only; all 137 tests pass. * fix(interactive): only render typed command values in fallback text, keep legacy value private * fix(feishu): gate document-comment command guidance on actual command action * docs(message-presentation): document command/callback value fallback visibility * fix(feishu): omit command guidance when URL overrides fallback command text * docs: regenerate docs_map.md * fix(interactive): exclude disabled buttons from fallback command rendering and guidance * fix(interactive): extract hasRenderedCommandAction, exclude disabled buttons from command fallback * fix(feishu): preserve command guidance marker through core presentation rendering * fix(feishu): type-narrow channelData.feishu with isRecord before reading rendered-command marker * fix(feishu): move hasRenderedCommandAction from public SDK into Feishu plugin as local helper Keep the helper local to the only caller (Feishu outbound) instead of adding a new public plugin SDK API contract. The shared fallback renderer in renderMessagePresentationFallbackText already inlines the same command-visibility logic; a local helper is sufficient for the Feishu comment-thread guidance gate. * refactor(feishu): tighten fallback command marker --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -278,6 +278,39 @@ describe("interactive payload helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves command values in button fallback text while keeping callback values private", () => {
|
||||
const presentation = {
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons" as const,
|
||||
buttons: [
|
||||
{ label: "Approve", value: "/approve req_1 allow-once" },
|
||||
{ label: "Deny", action: { type: "command" as const, command: "/approve req_1 deny" } },
|
||||
{ label: "Ignore", action: { type: "callback" as const, value: "ignore_123" } },
|
||||
{ label: "Docs", url: "https://example.com/docs" },
|
||||
{ label: "Disabled", disabled: true },
|
||||
{
|
||||
label: "DisabledCmd",
|
||||
disabled: true,
|
||||
action: { type: "command" as const, command: "/test" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(renderMessagePresentationFallbackText({ presentation })).toBe(
|
||||
[
|
||||
"- Approve",
|
||||
"- Deny: `/approve req_1 deny`",
|
||||
"- Ignore",
|
||||
"- Docs: https://example.com/docs",
|
||||
"- Disabled",
|
||||
"- DisabledCmd",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps divider-only fallback empty unless a send transport fallback is requested", () => {
|
||||
const presentation = {
|
||||
blocks: [{ type: "divider" as const }],
|
||||
|
||||
@@ -503,6 +503,21 @@ export function interactiveReplyToPresentation(
|
||||
return blocks.length > 0 ? { blocks } : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render presentation blocks as plain-text fallback for channels that do not
|
||||
* support native interactive controls.
|
||||
*
|
||||
* Text and context blocks are rendered as-is. Buttons with a `command`-typed
|
||||
* action render as `label: \`command\`` so the value is copyable. Buttons with
|
||||
* a `callback` action, legacy `value`, or `select` options render as label-only
|
||||
* to keep opaque callback values private. Disabled buttons render as label-only
|
||||
* regardless of action type, since they are not actionable.
|
||||
*
|
||||
* Downstream consumers should not claim a manual command is available unless
|
||||
* they verify one was actually rendered.
|
||||
*
|
||||
* Exported through the plugin SDK for channel adapters.
|
||||
*/
|
||||
export function renderMessagePresentationFallbackText(params: {
|
||||
presentation?: MessagePresentation;
|
||||
emptyFallback?: string | null;
|
||||
@@ -529,7 +544,17 @@ export function renderMessagePresentationFallbackText(params: {
|
||||
const labels = block.buttons
|
||||
.map((button) => {
|
||||
const targetUrl = button.url ?? button.webApp?.url ?? button.web_app?.url;
|
||||
return targetUrl ? `${button.label}: ${targetUrl}` : button.label;
|
||||
if (targetUrl) {
|
||||
return `${button.label}: ${targetUrl}`;
|
||||
}
|
||||
const controlValue =
|
||||
button.action?.type === "command"
|
||||
? resolveMessagePresentationControlValue(button)
|
||||
: undefined;
|
||||
if (controlValue && !button.disabled) {
|
||||
return `${button.label}: \`${controlValue}\``;
|
||||
}
|
||||
return button.label;
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (labels.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user