// 表情回执策略(用户指令 2026-08-07): // - 收到用户消息 → 随机池取一枚表情("已收到"即时反馈) // - 回合/任务完成 → 对触发消息打 DONE 表情 // 约束:DONE 永不参与随机池(任务完成标记专用)。 export const DONE_EMOJI = "DONE" as const; /** * 飞书 reaction 接口支持的全部 emoji_type(open.feishu.cn …/emojis-introduce, * 2026-08-13 抓取)。大小写敏感(Fire 有效、FIRE 无效;AWESOMEN 有效、 * AWESOME 无效)。用于过滤配置池里的非法值——否则随机抽到无效类型就 400 * (code 231001 "reaction type is invalid",用户"收到表情"落空)。 */ export const VALID_EMOJI_TYPES: ReadonlySet = new Set([ "OK", "THUMBSUP", "THANKS", "MUSCLE", "FINGERHEART", "APPLAUSE", "FISTBUMP", "JIAYI", "DONE", "SMILE", "BLUSH", "LAUGH", "SMIRK", "LOL", "FACEPALM", "LOVE", "WINK", "PROUD", "WITTY", "SMART", "SCOWL", "THINKING", "SOB", "CRY", "ERROR", "NOSEPICK", "HAUGHTY", "SLAP", "SPITBLOOD", "TOASTED", "GLANCE", "DULL", "INNOCENTSMILE", "JOYFUL", "WOW", "TRICK", "YEAH", "ENOUGH", "TEARS", "EMBARRASSED", "KISS", "SMOOCH", "DROOL", "OBSESSED", "MONEY", "TEASE", "SHOWOFF", "COMFORT", "CLAP", "PRAISE", "STRIVE", "XBLUSH", "SILENT", "WAVE", "WHAT", "FROWN", "SHY", "DIZZY", "LOOKDOWN", "CHUCKLE", "WAIL", "CRAZY", "WHIMPER", "HUG", "BLUBBER", "WRONGED", "HUSKY", "SHHH", "SMUG", "ANGRY", "HAMMER", "SHOCKED", "TERROR", "PETRIFIED", "SKULL", "SWEAT", "SPEECHLESS", "SLEEP", "DROWSY", "YAWN", "SICK", "PUKE", "BETRAYED", "HEADSET", "EatingFood", "MeMeMe", "Sigh", "Typing", "Lemon", "Get", "LGTM", "OnIt", "OneSecond", "VRHeadset", "YouAreTheBest", "SALUTE", "SHAKE", "HIGHFIVE", "UPPERLEFT", "ThumbsDown", "SLIGHT", "TONGUE", "EYESCLOSED", "RoarForYou", "CALF", "BEAR", "BULL", "RAINBOWPUKE", "ROSE", "HEART", "PARTY", "LIPS", "BEER", "CAKE", "GIFT", "CUCUMBER", "Drumstick", "Pepper", "CANDIEDHAWS", "BubbleTea", "Coffee", "Yes", "No", "OKR", "CheckMark", "CrossMark", "MinusOne", "Hundred", "AWESOMEN", "Pin", "Alarm", "Loudspeaker", "Trophy", "Fire", "BOMB", "Music", "XmasTree", "Snowman", "XmasHat", "FIREWORKS", "REDPACKET", "FORTUNE", "LUCK", "FIRECRACKER", "StickyRiceBalls", "HEARTBROKEN", "POOP", "StatusFlashOfInspiration", "CLEAVER", "Soccer", "Basketball", "GeneralDoNotDisturb", "Status_PrivateMessage", "GeneralInMeetingBusy", "StatusReading", "StatusInFlight", "GeneralBusinessTrip", "GeneralWorkFromHome", "StatusEnjoyLife", "GeneralTravellingCar", "StatusBus", "GeneralSun", "GeneralMoonRest", "MoonRabbit", "Mooncake", "JubilantRabbit", "TV", "Movie", "Pumpkin", "BeamingFace", "Delighted", "ColdSweat", "FullMoonFace", "Partying", "GoGoGo", "ThanksFace", "SaluteFace", "Shrug", "ClownFace", "HappyDragon", ]); /** 默认随机表情池(飞书 reaction emoji_type,全部排除 DONE)。 * 2026-08-08 修复:FIRE → Fire(飞书 emoji_type 大小写敏感,FIRE 无效)。 */ export const REACTION_POOL: readonly string[] = [ "THUMBSUP", "OK", "HEART", "LAUGH", "SMILE", "WOW", "CLAP", "Fire", ] as const; /** * 从池中随机取一枚表情。 * - 传入池为空 / 全为 DONE / 全为无效类型 → 回退默认池(默认池已排除 DONE) * - 无效类型(非飞书 emoji_type)会被过滤,避免 400(code 231001) * - 无论池内容如何,返回值永不为 DONE * - rng 可注入(测试确定性),默认 Math.random */ export function pickRandomReaction( pool: readonly string[] | undefined = REACTION_POOL, rng: () => number = Math.random, ): string { const source = pool && pool.length > 0 ? pool : REACTION_POOL; // 2026-08-13:过滤飞书不支持的 emoji_type(如 FIRE/AMAZE/AWESOME/COOL)—— // 此前配置池含非法值时随机抽到就 400,用户收不到"已收到"表情。 const valid = source.filter( (e) => e !== DONE_EMOJI && VALID_EMOJI_TYPES.has(e), ); const usable = valid.length > 0 ? valid : REACTION_POOL; const idx = Math.floor(rng() * usable.length) % usable.length; // idx 恒在界内(对 length 取模),非空断言仅为满足 noUncheckedIndexedAccess return usable[idx] as string; }