@qihoo/tuitui-openclaw-channel 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
2
- import { emptyPluginConfigSchema } from 'openclaw/plugin-sdk';
2
+ import { emptyPluginConfigSchema } from 'openclaw/plugin-sdk/core';
3
3
  import { createTuiTuiChannelPlugin } from './src/channel';
4
4
  import { registerTuituiTools } from './src/tools';
5
5
  import { CHANNEL_ID, CHANNEL_NAME } from './src/const';
@@ -8,7 +8,7 @@ import { id } from './openclaw.plugin.json';
8
8
  const plugin = {
9
9
  id, // plugin id not is CHANNEL_ID
10
10
  name: CHANNEL_NAME,
11
- description: `${CHANNEL_NAME} chat integration for OpenClaw via webhook`,
11
+ description: `${CHANNEL_NAME} chat integration for OpenClaw via WebSocket`,
12
12
  configSchema: emptyPluginConfigSchema(),
13
13
  register(api: OpenClawPluginApi) {
14
14
  console.log(`[${CHANNEL_ID}] Plugin.register Before.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qihoo/tuitui-openclaw-channel",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "maintainers": [
5
5
  {
6
6
  "name": "huzunjie",
@@ -17,6 +17,9 @@
17
17
  ],
18
18
  "description": "TuiTui channel plugin for OpenClaw",
19
19
  "type": "module",
20
+ "peerDependencies": {
21
+ "openclaw": ">=2026.3.13"
22
+ },
20
23
  "dependencies": {
21
24
  "@sinclair/typebox": "^0.34.48",
22
25
  "ws": "^8.13.0"
package/src/accounts.ts CHANGED
@@ -1,9 +1,7 @@
1
- import { parseAllowFroms } from './utils';
2
- import { capabilities, configSchema, baseFildsDefault } from './confs';
3
- import { CHANNEL_ID,} from "./const";
4
- import { DEFAULT_ACCOUNT_ID,} from 'openclaw/plugin-sdk';
5
-
6
- const isEnabled = (val: any) => val === undefined || !!val;
1
+ import { parseAllowFroms, isEnabled } from './utils';
2
+ import { baseFildsDefault } from './confs';
3
+ import { CHANNEL_ID } from './const';
4
+ import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk/account-id';
7
5
 
8
6
  export const resolveAccount = (cfg: any, accountId?: string | null) => {
9
7
  const channelConfig = cfg?.channels?.[CHANNEL_ID] || {};
@@ -21,5 +19,6 @@ export const resolveAccount = (cfg: any, accountId?: string | null) => {
21
19
  groupAllowFrom: parseAllowFroms(acct?.groupAllowFrom || baseFildsDefault.groupAllowFrom),
22
20
  requireMention: isEnabled(acct?.requireMention ?? baseFildsDefault.requireMention),
23
21
  channelContext: acct?.channelContext || baseFildsDefault.channelContext,
22
+ emojiReaction: isEnabled(acct?.emojiReaction ?? baseFildsDefault.emojiReaction),
24
23
  };
25
24
  };
package/src/channel.ts CHANGED
@@ -5,11 +5,8 @@
5
5
  * Supports single chat (text, image, voice, file) and group chat with @mentions.
6
6
  */
7
7
  import WebSocket from 'ws';
8
- import {
9
- DEFAULT_ACCOUNT_ID,
10
- setAccountEnabledInConfigSection,
11
- deleteAccountFromConfigSection,
12
- } from 'openclaw/plugin-sdk';
8
+ import { DEFAULT_ACCOUNT_ID } from 'openclaw/plugin-sdk/account-id';
9
+ import { setAccountEnabledInConfigSection, deleteAccountFromConfigSection } from 'openclaw/plugin-sdk/core';
13
10
  import { CHANNEL_ID, CHANNEL_NAME } from "./const";
14
11
  import {
15
12
  checkAccount,
@@ -21,9 +18,8 @@ import {
21
18
  import { handleInboundMessage } from './inbound';
22
19
  import { capabilities, configSchema, baseFildsDefault } from './confs';
23
20
  import { resolveAccount } from "./accounts"
21
+ import { isEnabled } from './utils';
24
22
 
25
-
26
- const isEnabled = (val: any) => val === undefined || !!val;
27
23
  const isConfigured = (account: any)=> !!(account?.appId && account?.appSecret);
28
24
 
29
25
  const wsReadyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'] as const;
package/src/confs.ts CHANGED
@@ -39,10 +39,13 @@ const baseFields = {
39
39
  },
40
40
  channelContext: {
41
41
  type: 'string',
42
- default: 'channel',
42
+ default: 'thread',
43
43
  enum: ['channel', 'thread']
44
44
  },
45
-
45
+ emojiReaction: {
46
+ type: 'boolean',
47
+ default: true,
48
+ },
46
49
  };
47
50
  const baseFieldsKeys = Object.keys(baseFields);
48
51
  export const baseFildsDefault = {} as Record<string, any>;
@@ -86,7 +89,7 @@ const fieldsUiHints = {
86
89
  advanced: true,
87
90
  },
88
91
  groupAllowFrom: {
89
- help: '群组/团队白名单-群ID和团队ID(仅在 groupPolicy=allowlist 生效)',
92
+ help: '群组/团队白名单-包含群ID、团队ID或频道ID(仅在 groupPolicy=allowlist 生效)',
90
93
  order: 14,
91
94
  advanced: true,
92
95
  },
@@ -95,9 +98,14 @@ const fieldsUiHints = {
95
98
  order: 15,
96
99
  advanced: true,
97
100
  },
101
+ emojiReaction: {
102
+ help: '在收到消息后,大模型给出反应结果前,先对原消息发送一个”收到“的表情回复。',
103
+ order: 16,
104
+ advanced: true,
105
+ },
98
106
  };
99
107
 
100
- /* 多账户管理 - 暂不开放,先保留代码逻辑
108
+ /* 多账户管理 */
101
109
  const accountsFieldsUiHints = {} as Record<string, any>;
102
110
  function isValidObjKey(key: string | number | symbol , object: object): key is keyof typeof object {
103
111
  return key in object;
@@ -105,7 +113,6 @@ function isValidObjKey(key: string | number | symbol , object: object): key is k
105
113
  for (const k in fieldsUiHints) {
106
114
  if (isValidObjKey(k, fieldsUiHints)) accountsFieldsUiHints[`accounts.*.${k}` as string] = fieldsUiHints[k];
107
115
  }
108
- */
109
116
 
110
117
  export const configSchema = {
111
118
  schema: {
@@ -113,7 +120,7 @@ export const configSchema = {
113
120
  additionalProperties: false,
114
121
  properties: {
115
122
  ...fields,
116
- /* 基础配置信息中,准备 accounts 配置支持多账户管理,以备后续允许用户配置多个推推机器人账号
123
+ /* 基础配置信息中,准备 accounts 配置支持多账户管理 */
117
124
  accounts: {
118
125
  type: 'object',
119
126
  additionalProperties: {
@@ -121,17 +128,17 @@ export const configSchema = {
121
128
  additionalProperties: false,
122
129
  properties: { ...fields },
123
130
  },
124
- }, */
131
+ },
125
132
  },
126
133
  },
127
134
  uiHints: {
128
135
  ...fieldsUiHints,
129
- /* 基础配置信息中,准备 accounts 配置支持多账户管理,以备后续允许用户配置多个推推机器人账号
136
+ /* 基础配置信息中,准备 accounts 配置支持多账户管理 */
130
137
  accounts: {
131
138
  help: 'Accounts(多账户配置)',
132
139
  order: 30,
133
140
  advanced: true
134
141
  },
135
- ...accountsFieldsUiHints, */
142
+ ...accountsFieldsUiHints,
136
143
  },
137
144
  };
@@ -0,0 +1,69 @@
1
+ import {
2
+ DEFAULT_GROUP_HISTORY_LIMIT,
3
+ recordPendingHistoryEntryIfEnabled,
4
+ type HistoryEntry,
5
+ } from "openclaw/plugin-sdk/reply-history";
6
+
7
+ // 每个 accountId 独立的 group history map,防止多账户冲突
8
+ const accountGroupHistories = new Map<string, Map<string, HistoryEntry[]>>();
9
+
10
+ /**
11
+ * 记录没有被@,被忽略的群消息到对应 account 的 pending history 中
12
+ */
13
+ export async function addUnmentionedHistory(apiRuntime, accountId: string, chatId: string, tuituiUserName: any, tuituiAccount: string, text: any, timestamp :any) {
14
+ const cfg = await apiRuntime.config.loadConfig();
15
+ const senderDesc = tuituiUserName ? `${tuituiUserName} (${tuituiAccount})` : tuituiAccount;
16
+
17
+ const entry: HistoryEntry = {
18
+ sender: senderDesc,
19
+ body: text,
20
+ timestamp: timestamp ? Number(timestamp) : Date.now(),
21
+ };
22
+
23
+ // 为每个 accountId 维护独立的 group history map
24
+ let groupHistories = accountGroupHistories.get(accountId);
25
+ if (!groupHistories) {
26
+ groupHistories = new Map<string, HistoryEntry[]>();
27
+ accountGroupHistories.set(accountId, groupHistories);
28
+ }
29
+
30
+ const historyLimit = resolveHistoryLimit(cfg);
31
+
32
+ // 这个API内部没啥神秘的,只是帮你维护map变量。openclaw本身不持有这些数据。
33
+ recordPendingHistoryEntryIfEnabled({
34
+ historyMap: groupHistories,
35
+ historyKey: chatId,
36
+ limit: historyLimit,
37
+ entry: entry,
38
+ });
39
+ }
40
+
41
+ /**
42
+ * 获取指定 account 和群组的 pending history,格式化为 ctx.InboundHistory 所需的格式
43
+ */
44
+ export function popUnmentionedHistories(accountId: string, chatId: string): {sender: string, body: string, timestamp?: number}[] {
45
+ const groupHistories = accountGroupHistories.get(accountId);
46
+ if (!groupHistories) {
47
+ return [];
48
+ }
49
+ const his = groupHistories.get(chatId);
50
+ if (!his) {
51
+ return [];
52
+ }
53
+
54
+ groupHistories.delete(chatId);
55
+
56
+ return his.map((entry) => ({
57
+ sender: entry.sender,
58
+ body: entry.body,
59
+ timestamp: entry.timestamp,
60
+ }));
61
+ }
62
+
63
+ /**
64
+ * 从配置中解析 history limit
65
+ * dashboard->messages->groupChat->historyLimit
66
+ */
67
+ export function resolveHistoryLimit(cfg: any): number {
68
+ return cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT;
69
+ }
package/src/inbound.ts CHANGED
@@ -23,6 +23,10 @@ import {
23
23
  teamsParseChatId,
24
24
  } from "./outbound";
25
25
  import { parseAllowFroms } from './utils';
26
+ import {
27
+ addUnmentionedHistory,
28
+ popUnmentionedHistories,
29
+ } from "./histories";
26
30
 
27
31
  /** 子函数共享的可变上下文,子函数直接修改字段,外层读取结果 */
28
32
  interface ChatPayload {
@@ -30,12 +34,14 @@ interface ChatPayload {
30
34
  chatId: string | undefined;
31
35
  text: string | undefined;
32
36
  groupName: string | undefined;
37
+ channelName: string | undefined;
33
38
  mediaUrls: string[] | undefined;
34
39
  replyToId: string | undefined; // 引用ID
35
40
  msgId?: string; // 消息ID
36
41
  tuituiAccount: string;
37
42
  tuituiUid: string | undefined;
38
43
  tuituiUserName: string | undefined;
44
+ timestamp: Number | undefined;
39
45
  }
40
46
 
41
47
  export interface InboundAccount {
@@ -48,6 +54,7 @@ export interface InboundAccount {
48
54
  groupAllowFrom: string[];
49
55
  requireMention: boolean;
50
56
  channelContext: string;
57
+ emojiReaction: boolean;
51
58
  }
52
59
  export interface InboundHandlerOptions {
53
60
  json: any
@@ -226,6 +233,7 @@ const parseAndVerifyPayload: Record<string, Function> = {
226
233
  payload.chatId = chatId;
227
234
  payload.groupName = msgData.group_name;
228
235
  payload.msgId = msgData.msgid;
236
+ payload.text = buildMessageBody(msgData);
229
237
  log?.debug?.(
230
238
  `[${CHANNEL_ID}] AccountId: ${accountId}, inbound group_chat:
231
239
  tuituiAccount=${payload.tuituiAccount},
@@ -248,7 +256,8 @@ const parseAndVerifyPayload: Record<string, Function> = {
248
256
  }
249
257
 
250
258
  if (!msgData.at_me && account.requireMention) {
251
- log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, ignore group message (not mentioned)`);
259
+ log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, ignore group message (not mentioned), add to history key: ${chatId}`);
260
+ await addUnmentionedHistory(apiRuntime, accountId, chatId, payload.tuituiUserName, payload.tuituiAccount, payload.text, payload.timestamp);
252
261
  return false;
253
262
  }
254
263
 
@@ -267,19 +276,20 @@ const parseAndVerifyPayload: Record<string, Function> = {
267
276
  return false;
268
277
  }
269
278
 
270
- payload.text = buildMessageBody(msgData);
279
+
271
280
 
272
281
  return true;
273
282
  },
274
283
  teams_post_create: async (payload: ChatPayload, msgData: any, account: InboundAccount, apiRuntime: any, log: any): Promise<boolean> => {
275
284
  const { accountId, groupPolicy, groupAllowFrom } = account;
276
285
  payload.chatType = CHAT_TYPE_CHANNEL;
277
- const { team_id, channel_id, parent_id, post_id, content } = msgData;
286
+ const { team_id, channel_id, parent_id, post_id, content, team_name, channel_name } = msgData;
278
287
  const thread_id = (parent_id && parent_id != "0")?parent_id: post_id;
279
288
  const chatId = teamsBuildChatId(team_id, channel_id, thread_id);
280
289
  payload.chatId = chatId;
281
290
  payload.msgId = post_id;
282
291
  payload.text = content;
292
+ payload.channelName = channel_name;
283
293
  payload.replyToId = "";
284
294
  log?.debug?.(
285
295
  `[${CHANNEL_ID}] AccountId: ${accountId}, inbound teams:
@@ -304,11 +314,18 @@ const parseAndVerifyPayload: Record<string, Function> = {
304
314
  }
305
315
 
306
316
  if (!msgData.at_me && account.requireMention) {
307
- log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, ignore teams post (not mentioned)`);
317
+ log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, ignore teams post (not mentioned), add to history key: ${chatId}`);
318
+ await addUnmentionedHistory(apiRuntime, accountId, chatId, payload.tuituiUserName, payload.tuituiAccount, payload.text, payload.timestamp);
308
319
  return false;
309
320
  }
310
321
 
311
- if (!groupAllowFrom.includes(String(team_id))) {
322
+ if (!groupAllowFrom.includes(String(team_id)) && !groupAllowFrom.includes(String(channel_id)) ) {
323
+ if (!msgData.at_me) {
324
+ // 解决这个case:requireMention=false时,团队里发任意帖子都会回复加白,搞得没法用
325
+ // 要求必须@才触发回复加白
326
+ log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, ignore teams post (not mentioned)`);
327
+ return false;
328
+ }
312
329
  if (needPairingThrottle(accountId, chatId)) {
313
330
  log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, teams pairing throttled for teamsChatId=${chatId}`);
314
331
  return false;
@@ -317,7 +334,7 @@ const parseAndVerifyPayload: Record<string, Function> = {
317
334
  account,
318
335
  chatId,
319
336
  payload.chatType,
320
- `当前openclaw(AccountId: ${accountId})群聊/团队策略为白名单,需要主人在群白名单(Group Allow From)增加当前团队ID:\n${team_id}`,
337
+ `当前openclaw(AccountId: ${accountId})群聊/团队策略为白名单,需要主人在群白名单(Group Allow From)增加当前团队ID: ${team_id} 或者频道ID: ${channel_id} 如果配置团队ID指当前团队下所有频道都可以使用,如果配置频道ID则仅此频道可使用`,
321
338
  'tuitui.groupPolicy.reply',
322
339
  );
323
340
  return false;
@@ -349,11 +366,13 @@ export async function handleInboundMessage({ json, account, apiRuntime, log }: I
349
366
  chatId: undefined,
350
367
  text: undefined,
351
368
  groupName: undefined,
369
+ channelName: undefined,
352
370
  mediaUrls: getMediaUrls(msgData),
353
371
  replyToId: ref?.is_me && ref?.msgid ? ref.msgid : undefined,
354
372
  tuituiAccount: msg.user_account || "",
355
373
  tuituiUid: msg.uid || "",
356
374
  tuituiUserName: msg.user_name || "",
375
+ timestamp: msg.timestamp ? parseInt(msg.timestamp, 10)*1000 : undefined
357
376
  };
358
377
  const wsEvent = json.body.event;
359
378
  // 按event类型,标准化并校验基础数据
@@ -369,8 +388,10 @@ export async function handleInboundMessage({ json, account, apiRuntime, log }: I
369
388
  return;
370
389
  }
371
390
 
372
- // 因为回复较慢,先回复一个表情
373
- await tuituiEmojiReaction(account, payload.chatId, payload.chatType, payload.msgId, '收到');
391
+ if (account.emojiReaction) {
392
+ // 因为回复较慢,先回复一个表情
393
+ await tuituiEmojiReaction(account, payload.chatId, payload.chatType, payload.msgId, '收到');
394
+ }
374
395
 
375
396
  // 路由判断
376
397
  const routeSenderFrom = payload.tuituiAccount || payload.tuituiUid || 'unknown';
@@ -379,6 +400,14 @@ export async function handleInboundMessage({ json, account, apiRuntime, log }: I
379
400
  const sessionKey = getSessionKey(cfg, payload, account, apiRuntime);
380
401
  log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, dispatching to agent session=${sessionKey}, chatType: ${payload.chatType}, chatId ${payload.chatId}, routeSenderFrom: ${routeSenderFrom}`);
381
402
 
403
+ if(payload.text?.startsWith("/new") || payload.text?.endsWith("/new")) {
404
+ // 清理agent上下文时,同时清空历史
405
+ popUnmentionedHistories(accountId, payload.chatId);
406
+ }
407
+
408
+ // 字段意义 参考 openclaw 代码
409
+ // src\auto-reply\reply\inbound-meta.ts
410
+ // buildInboundUserContextPrefix()
382
411
  const ctx: any = {
383
412
  Body: payload.text! || ' ',
384
413
  From: String(routeSenderFrom),
@@ -392,17 +421,24 @@ export async function handleInboundMessage({ json, account, apiRuntime, log }: I
392
421
  Surface: CHANNEL_ID,
393
422
  Provider: CHANNEL_ID,
394
423
  SenderName: payload.tuituiUserName,
395
- MsgUname: payload.tuituiAccount,
424
+ SenderId: payload.tuituiAccount,
425
+ SenderUsername: payload.tuituiAccount,
396
426
  UserAccount: payload.tuituiAccount,
427
+ Timestamp: payload.timestamp,
397
428
  CommandAuthorized: true, // 允许 /new 等内置命令
398
429
  };
399
430
  if (payload.chatType == CHAT_TYPE_GROUP && payload.chatId) {
400
431
  ctx.GroupSubject = payload.groupName;
401
432
  ctx.GroupId = payload.chatId;
433
+ ctx.InboundHistory = popUnmentionedHistories(accountId, payload.chatId);
434
+ }
435
+ if (payload.chatType == CHAT_TYPE_CHANNEL) {
436
+ ctx.GroupChannel = payload.channelName;
437
+ ctx.InboundHistory = popUnmentionedHistories(accountId, payload.chatId);
402
438
  }
403
439
  if (payload.mediaUrls?.length) ctx.MediaUrls = payload.mediaUrls;
404
440
  if (payload.replyToId) ctx.ReplyToId = payload.replyToId;
405
441
 
406
- log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, handleInboundMessage dispatchReply payload:`, JSON.stringify(payload, null, ' '));
442
+ log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, handleInboundMessage dispatchReply ctx: ${JSON.stringify(ctx, null, ' ')}`);
407
443
  dispatchReply(ctx, cfg, account, payload, apiRuntime, log);
408
444
  }
package/src/outbound.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, existsSync, statSync } from 'node:fs';
2
2
  import { basename } from 'node:path';
3
- import { fetchWithSsrFGuard } from 'openclaw/plugin-sdk';
3
+ import { fetchWithSsrFGuard } from 'openclaw/plugin-sdk/tlon';
4
4
  import { CHANNEL_ID } from "./const";
5
5
 
6
6
  import type {
@@ -491,6 +491,7 @@ export async function sendMediaMsg(
491
491
  atList?: string[],
492
492
  ): Promise<void> {
493
493
  if (!chatId) return console.error(`[${CHANNEL_ID}] sendMediaMsg Error ${auditCtx}: Missing "target"`);
494
+ console.log(`[${CHANNEL_ID}] sendMediaMsg ${chatType} ${chatId} ${auditCtx} uploading`);
494
495
  // Check if mediaUrl looks like an image
495
496
  const isImage = /^data:image\//i.test(mediaUrl) || /\.(jpg|jpeg|png|gif)(?:$|[?#])/i.test(mediaUrl);
496
497
  const mediaType = isImage ? 'image' : 'file';
package/src/tools.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
- import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk";
3
- import { DEFAULT_ACCOUNT_ID} from 'openclaw/plugin-sdk';
2
+ import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
4
3
  import { CHANNEL_ID} from "./const";
5
4
  import { resolveAccount } from "./accounts"
6
- import { Type, type Static } from "@sinclair/typebox";
5
+ import { Type } from "@sinclair/typebox";
7
6
 
8
7
  import {CHAT_TYPE_DIRECT,CHAT_TYPE_GROUP,CHAT_TYPE_CHANNEL,ChatType, getChatRecord} from "./outbound"
9
8
 
@@ -66,13 +65,13 @@ const tuituiToolFactory = (ctx: OpenClawPluginToolContext) => {
66
65
  label: "tuitui IM",
67
66
  description: "推推(tuitui) 聊天记录获取,可查询群聊和私聊的聊天记录。\n\n",
68
67
  parameters: tuitui_im_get_messages_schema,
69
- execute: async (_toolCallId, params) => {
68
+ execute: async (_toolCallId: any, params: any) => {
70
69
  if(messageChannel != CHANNEL_ID) {
71
70
  console.log(`tuitui_im_get_messages(): bad channel ${messageChannel}`);
72
71
  return
73
72
  }
74
73
  console.log(`tuitui_im_get_messages(): agentAccountId: ${agentAccountId}, sessionKey: ${sessionKey}`, params);
75
- return await tuitui_im_get_messages(config, agentAccountId, params);
74
+ return await tuitui_im_get_messages(config, String(agentAccountId), params);
76
75
  },
77
76
  };
78
77
  };
package/src/utils.ts CHANGED
@@ -2,4 +2,6 @@
2
2
  export const parseAllowFroms = (allowFrom: any) : string[] => {
3
3
  const arr = Array.isArray(allowFrom) ? allowFrom : [];
4
4
  return arr.filter((v: any) => !!v).map((v: any) => String(v).toLowerCase().trim());
5
- }
5
+ }
6
+
7
+ export const isEnabled = (val: any) => val === undefined || !!val;