11// Parses poll command parameters into validated polling options.
2- import { parseStrictFiniteNumber } from "@openclaw/normalization-core/number-coercion" ;
3- import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce" ;
42import { readSnakeCaseParamRaw } from "./param-key.js" ;
53
64type PollCreationParamKind = "string" | "stringArray" | "positiveInteger" | "boolean" ;
@@ -24,65 +22,17 @@ type SharedPollCreationParamName = keyof typeof SHARED_POLL_CREATION_PARAM_DEFS;
2422export const SHARED_POLL_CREATION_PARAM_NAMES = Object . keys (
2523 SHARED_POLL_CREATION_PARAM_DEFS ,
2624) as SharedPollCreationParamName [ ] ;
27- const SHARED_POLL_CREATION_PARAM_KEY_SET = new Set (
28- SHARED_POLL_CREATION_PARAM_NAMES . map ( normalizePollParamKey ) ,
29- ) ;
30- const POLL_VOTE_PARAM_KEY_SET = new Set (
31- [ "pollId" , "pollOptionId" , "pollOptionIds" , "pollOptionIndex" , "pollOptionIndexes" ] . map (
32- normalizePollParamKey ,
33- ) ,
34- ) ;
3525
3626function readPollParamRaw ( params : Record < string , unknown > , key : string ) : unknown {
3727 return readSnakeCaseParamRaw ( params , key ) ;
3828}
3929
40- function normalizePollParamKey ( key : string ) : string {
41- return normalizeLowercaseStringOrEmpty ( key . replaceAll ( "_" , "" ) ) ;
42- }
43-
44- function isChannelPollCreationParamName ( key : string ) : boolean {
45- const normalized = normalizePollParamKey ( key ) ;
46- return (
47- normalized . startsWith ( "poll" ) &&
48- ! SHARED_POLL_CREATION_PARAM_KEY_SET . has ( normalized ) &&
49- ! POLL_VOTE_PARAM_KEY_SET . has ( normalized )
50- ) ;
51- }
52-
53- function hasExplicitUnknownPollValue ( key : string , value : unknown ) : boolean {
54- if ( value === true ) {
55- return true ;
56- }
57- if ( typeof value === "number" ) {
58- return Number . isFinite ( value ) && value !== 0 ;
59- }
60- if ( typeof value === "string" ) {
61- const trimmed = value . trim ( ) ;
62- if ( trimmed . length === 0 ) {
63- return false ;
64- }
65- if ( normalizePollParamKey ( key ) . includes ( "duration" ) ) {
66- const parsed = parseStrictFiniteNumber ( trimmed ) ;
67- return Number . isFinite ( parsed ) && parsed !== 0 ;
68- }
69- const normalized = normalizeLowercaseStringOrEmpty ( trimmed ) ;
70- return normalized !== "false" && normalized !== "0" ;
71- }
72- if ( Array . isArray ( value ) ) {
73- return value . some ( ( entry ) => hasExplicitUnknownPollValue ( key , entry ) ) ;
74- }
75- return false ;
76- }
77-
7830// Among the shared poll params, only the content-bearing fields (pollQuestion,
7931// pollOption) signal poll intent on their own. The modifier fields
80- // (pollDurationHours, pollMulti) are exposed by the shared `message` tool
81- // schema for both `send` and `poll` actions, so LLMs routinely echo their
82- // schema-implied defaults (`1`, `false`) on plain `send` calls — see issue
83- // for context. Treating those modifier defaults as "the agent meant to create
84- // a poll" produces false positives and blocks routine sends. The modifiers
85- // only count when accompanied by a content-bearing field.
32+ // (pollDurationHours, pollMulti) and channel-specific metadata
33+ // (pollDurationSeconds, pollPublic, pollAnonymous) are also exposed on the
34+ // shared `message` tool schema, so schema-padded plain sends may echo them.
35+ // Only content fields count here; action="poll" validates modifiers later.
8636const CONTENT_BEARING_SHARED_POLL_PARAM_NAMES = [ "pollQuestion" , "pollOption" ] as const ;
8737
8838function hasContentBearingPollCreationParam ( params : Record < string , unknown > ) : boolean {
@@ -108,17 +58,5 @@ function hasContentBearingPollCreationParam(params: Record<string, unknown>): bo
10858}
10959
11060export function hasPollCreationParams ( params : Record < string , unknown > ) : boolean {
111- if ( hasContentBearingPollCreationParam ( params ) ) {
112- return true ;
113- }
114- // Channel-specific poll-prefixed params (e.g. pollDurationSeconds,
115- // pollPublic) are not part of the shared schema, so an explicit value still
116- // indicates deliberate poll intent and continues to trigger the validator
117- // even without a pollQuestion/pollOption.
118- for ( const [ key , value ] of Object . entries ( params ) ) {
119- if ( isChannelPollCreationParamName ( key ) && hasExplicitUnknownPollValue ( key , value ) ) {
120- return true ;
121- }
122- }
123- return false ;
61+ return hasContentBearingPollCreationParam ( params ) ;
12462}
0 commit comments