@zlr_236/popo 0.0.1
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 +53 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +66 -0
- package/skills/popo-admin/SKILL.md +195 -0
- package/skills/popo-card/SKILL.md +571 -0
- package/skills/popo-group/SKILL.md +292 -0
- package/skills/popo-msg/SKILL.md +360 -0
- package/skills/popo-team/SKILL.md +296 -0
- package/src/accounts.ts +52 -0
- package/src/auth.ts +151 -0
- package/src/bot.ts +394 -0
- package/src/channel.ts +839 -0
- package/src/client.ts +118 -0
- package/src/config-schema.ts +79 -0
- package/src/crypto.ts +67 -0
- package/src/media.ts +623 -0
- package/src/monitor.ts +236 -0
- package/src/outbound.ts +133 -0
- package/src/policy.ts +93 -0
- package/src/probe.ts +29 -0
- package/src/reply-dispatcher.ts +141 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +430 -0
- package/src/targets.ts +68 -0
- package/src/team.ts +506 -0
- package/src/types.ts +48 -0
package/src/send.ts
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { PopoConfig, PopoSendResult } from "./types.js";
|
|
3
|
+
import { popoRequest } from "./client.js";
|
|
4
|
+
import { normalizePopoTarget } from "./targets.js";
|
|
5
|
+
|
|
6
|
+
export type SendPopoMessageParams = {
|
|
7
|
+
cfg: ClawdbotConfig;
|
|
8
|
+
to: string;
|
|
9
|
+
text: string;
|
|
10
|
+
atUids?: string[];
|
|
11
|
+
isAtAll?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Send a text message to POPO.
|
|
16
|
+
*/
|
|
17
|
+
export async function sendMessagePopo(params: SendPopoMessageParams): Promise<PopoSendResult> {
|
|
18
|
+
const { cfg, to, text, atUids, isAtAll } = params;
|
|
19
|
+
const popoCfg = cfg.channels?.popo as PopoConfig | undefined;
|
|
20
|
+
if (!popoCfg) {
|
|
21
|
+
throw new Error("POPO channel not configured");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const receiver = normalizePopoTarget(to);
|
|
25
|
+
if (!receiver) {
|
|
26
|
+
throw new Error(`Invalid POPO target: ${to}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const message: Record<string, unknown> = {
|
|
30
|
+
content: text,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (atUids && atUids.length > 0) {
|
|
34
|
+
message.atUids = atUids;
|
|
35
|
+
}
|
|
36
|
+
if (isAtAll) {
|
|
37
|
+
message.isAtAll = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const response = await popoRequest<{ msgInfo?: Record<string, string> }>({
|
|
41
|
+
cfg: popoCfg,
|
|
42
|
+
method: "POST",
|
|
43
|
+
path: "/open-apis/robots/v1/im/send-msg",
|
|
44
|
+
body: {
|
|
45
|
+
receiver,
|
|
46
|
+
msgType: "text",
|
|
47
|
+
message,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (response.errcode !== 0) {
|
|
52
|
+
throw new Error(`POPO send failed: ${response.errmsg || `errcode ${response.errcode}`}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const msgInfo = response.data?.msgInfo ?? {};
|
|
56
|
+
const messageId = msgInfo[receiver];
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
messageId,
|
|
60
|
+
sessionId: receiver,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type SendPopoRichTextParams = {
|
|
65
|
+
cfg: ClawdbotConfig;
|
|
66
|
+
to: string;
|
|
67
|
+
content: Array<{ tag: string; text?: string; href?: string; userId?: string }>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Send a rich text message to POPO.
|
|
72
|
+
*/
|
|
73
|
+
export async function sendRichTextPopo(params: SendPopoRichTextParams): Promise<PopoSendResult> {
|
|
74
|
+
const { cfg, to, content } = params;
|
|
75
|
+
const popoCfg = cfg.channels?.popo as PopoConfig | undefined;
|
|
76
|
+
if (!popoCfg) {
|
|
77
|
+
throw new Error("POPO channel not configured");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const receiver = normalizePopoTarget(to);
|
|
81
|
+
if (!receiver) {
|
|
82
|
+
throw new Error(`Invalid POPO target: ${to}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const response = await popoRequest<{ msgInfo?: Record<string, string> }>({
|
|
86
|
+
cfg: popoCfg,
|
|
87
|
+
method: "POST",
|
|
88
|
+
path: "/open-apis/robots/v1/im/send-msg",
|
|
89
|
+
body: {
|
|
90
|
+
receiver,
|
|
91
|
+
msgType: "rich_text",
|
|
92
|
+
message: { content },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (response.errcode !== 0) {
|
|
97
|
+
throw new Error(`POPO rich text send failed: ${response.errmsg || `errcode ${response.errcode}`}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const msgInfo = response.data?.msgInfo ?? {};
|
|
101
|
+
const messageId = msgInfo[receiver];
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
messageId,
|
|
105
|
+
sessionId: receiver,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type PopoCardOptions = {
|
|
110
|
+
enableForward?: boolean;
|
|
111
|
+
lastMessage?: string;
|
|
112
|
+
lastMessageI18n?: Record<string, string>;
|
|
113
|
+
compatibleMessage?: string;
|
|
114
|
+
compatibleMessageI18n?: Record<string, string>;
|
|
115
|
+
withReadFlag?: boolean;
|
|
116
|
+
removeLastMessagePrefix?: boolean;
|
|
117
|
+
msgMenuConfigProtocol?: string;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export type SendPopoCardParams = {
|
|
121
|
+
cfg: ClawdbotConfig;
|
|
122
|
+
to: string;
|
|
123
|
+
templateUuid: string;
|
|
124
|
+
instanceUuid: string;
|
|
125
|
+
callBackConfigKey?: string;
|
|
126
|
+
publicVariableMap?: Record<string, unknown>;
|
|
127
|
+
batchPrivateVariableMap?: Record<string, Record<string, unknown>>;
|
|
128
|
+
options?: PopoCardOptions;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Send a card message to POPO.
|
|
133
|
+
*/
|
|
134
|
+
export async function sendCardPopo(params: SendPopoCardParams): Promise<PopoSendResult> {
|
|
135
|
+
const { cfg, to, templateUuid, instanceUuid, callBackConfigKey, publicVariableMap, batchPrivateVariableMap, options } = params;
|
|
136
|
+
const popoCfg = cfg.channels?.popo as PopoConfig | undefined;
|
|
137
|
+
if (!popoCfg) {
|
|
138
|
+
throw new Error("POPO channel not configured");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const receiver = normalizePopoTarget(to);
|
|
142
|
+
if (!receiver) {
|
|
143
|
+
throw new Error(`Invalid POPO target: ${to}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const message: Record<string, unknown> = {
|
|
147
|
+
templateUuid,
|
|
148
|
+
instanceUuid,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
if (callBackConfigKey) {
|
|
152
|
+
message.callBackConfigKey = callBackConfigKey;
|
|
153
|
+
}
|
|
154
|
+
if (publicVariableMap) {
|
|
155
|
+
message.publicVariableMap = publicVariableMap;
|
|
156
|
+
}
|
|
157
|
+
if (batchPrivateVariableMap) {
|
|
158
|
+
message.batchPrivateVariableMap = batchPrivateVariableMap;
|
|
159
|
+
}
|
|
160
|
+
if (options) {
|
|
161
|
+
message.options = options;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const response = await popoRequest<{ msgInfo?: Record<string, string> }>({
|
|
165
|
+
cfg: popoCfg,
|
|
166
|
+
method: "POST",
|
|
167
|
+
path: "/open-apis/robots/v1/im/send-msg",
|
|
168
|
+
body: {
|
|
169
|
+
receiver,
|
|
170
|
+
msgType: "card",
|
|
171
|
+
message,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (response.errcode !== 0) {
|
|
176
|
+
throw new Error(`POPO card send failed: ${response.errmsg || `errcode ${response.errcode}`}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const msgInfo = response.data?.msgInfo ?? {};
|
|
180
|
+
const messageId = msgInfo[receiver];
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
messageId,
|
|
184
|
+
sessionId: receiver,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Convert markdown-like text to POPO rich text content array.
|
|
190
|
+
*/
|
|
191
|
+
export function textToRichTextContent(
|
|
192
|
+
text: string
|
|
193
|
+
): Array<{ tag: string; text?: string }> {
|
|
194
|
+
// For now, just wrap the entire text in a single text element
|
|
195
|
+
// More sophisticated parsing could handle bold, links, etc.
|
|
196
|
+
return [{ tag: "text", text }];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export type CreatePopoStreamCardParams = {
|
|
200
|
+
cfg: ClawdbotConfig;
|
|
201
|
+
to: string;
|
|
202
|
+
templateUuid: string;
|
|
203
|
+
instanceUuid: string;
|
|
204
|
+
robotAccount: string;
|
|
205
|
+
fromUser?: string;
|
|
206
|
+
sessionType?: number;
|
|
207
|
+
callbackKey?: string;
|
|
208
|
+
initialContent?: string;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export type PopoStreamCardResult = {
|
|
212
|
+
success: boolean;
|
|
213
|
+
instanceUuid: string;
|
|
214
|
+
messageId?: string;
|
|
215
|
+
error?: string;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Create a streaming card message in POPO.
|
|
220
|
+
* Streaming cards allow real-time content updates via updateStreamCardPopo().
|
|
221
|
+
*/
|
|
222
|
+
export async function createStreamCardPopo(
|
|
223
|
+
params: CreatePopoStreamCardParams
|
|
224
|
+
): Promise<PopoStreamCardResult> {
|
|
225
|
+
const {
|
|
226
|
+
cfg,
|
|
227
|
+
to,
|
|
228
|
+
templateUuid,
|
|
229
|
+
instanceUuid,
|
|
230
|
+
robotAccount,
|
|
231
|
+
fromUser = "",
|
|
232
|
+
sessionType = 1,
|
|
233
|
+
callbackKey,
|
|
234
|
+
initialContent = "AI正在思考中...",
|
|
235
|
+
} = params;
|
|
236
|
+
|
|
237
|
+
const popoCfg = cfg.channels?.popo as PopoConfig | undefined;
|
|
238
|
+
if (!popoCfg) {
|
|
239
|
+
throw new Error("POPO channel not configured");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const receiver = normalizePopoTarget(to);
|
|
243
|
+
if (!receiver) {
|
|
244
|
+
throw new Error(`Invalid POPO target: ${to}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const message: Record<string, unknown> = {
|
|
248
|
+
templateUuid,
|
|
249
|
+
instanceUuid,
|
|
250
|
+
options: {
|
|
251
|
+
enableForward: false,
|
|
252
|
+
lastMessage: initialContent,
|
|
253
|
+
},
|
|
254
|
+
publicVariableMap: {
|
|
255
|
+
references: [],
|
|
256
|
+
resultStream: {
|
|
257
|
+
seq: 0,
|
|
258
|
+
content: "",
|
|
259
|
+
status: 0, // 0 = pending
|
|
260
|
+
},
|
|
261
|
+
thinkStream: {
|
|
262
|
+
seq: 0,
|
|
263
|
+
content: "",
|
|
264
|
+
status: 0,
|
|
265
|
+
},
|
|
266
|
+
thinkTitleText: {
|
|
267
|
+
"zh-CN": "思考中...",
|
|
268
|
+
"en-US": "Thinking...",
|
|
269
|
+
"ja-JP": "考え中...",
|
|
270
|
+
},
|
|
271
|
+
isGoodChecked: false,
|
|
272
|
+
isBadChecked: false,
|
|
273
|
+
extraParams: {
|
|
274
|
+
aiMessageId: "",
|
|
275
|
+
customerMessageId: "",
|
|
276
|
+
robotKey: robotAccount,
|
|
277
|
+
openid: fromUser,
|
|
278
|
+
type: String(sessionType),
|
|
279
|
+
content: "",
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
if (callbackKey) {
|
|
285
|
+
message.callBackConfigKey = callbackKey;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const response = await popoRequest<{ msgInfo?: Record<string, string> }>({
|
|
289
|
+
cfg: popoCfg,
|
|
290
|
+
method: "POST",
|
|
291
|
+
path: "/open-apis/robots/v1/im/send-msg",
|
|
292
|
+
body: {
|
|
293
|
+
receiver,
|
|
294
|
+
msgType: "card",
|
|
295
|
+
message,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (response.errcode !== 0) {
|
|
300
|
+
return {
|
|
301
|
+
success: false,
|
|
302
|
+
instanceUuid,
|
|
303
|
+
error: response.errmsg || `errcode ${response.errcode}`,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const msgInfo = response.data?.msgInfo ?? {};
|
|
308
|
+
const messageId = msgInfo[receiver];
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
success: true,
|
|
312
|
+
instanceUuid,
|
|
313
|
+
messageId,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export type UpdatePopoStreamCardParams = {
|
|
318
|
+
cfg: ClawdbotConfig;
|
|
319
|
+
templateUuid: string;
|
|
320
|
+
instanceUuid: string;
|
|
321
|
+
content: string;
|
|
322
|
+
sequence: number;
|
|
323
|
+
isFinalize?: boolean;
|
|
324
|
+
isError?: boolean;
|
|
325
|
+
streamKey?: string;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Update a streaming card's content in real-time.
|
|
330
|
+
* Use this to stream AI-generated content or progress updates.
|
|
331
|
+
*/
|
|
332
|
+
export async function updateStreamCardPopo(
|
|
333
|
+
params: UpdatePopoStreamCardParams
|
|
334
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
335
|
+
const {
|
|
336
|
+
cfg,
|
|
337
|
+
templateUuid,
|
|
338
|
+
instanceUuid,
|
|
339
|
+
content,
|
|
340
|
+
sequence,
|
|
341
|
+
isFinalize = false,
|
|
342
|
+
isError = false,
|
|
343
|
+
streamKey = "resultStream",
|
|
344
|
+
} = params;
|
|
345
|
+
|
|
346
|
+
const popoCfg = cfg.channels?.popo as PopoConfig | undefined;
|
|
347
|
+
if (!popoCfg) {
|
|
348
|
+
throw new Error("POPO channel not configured");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const response = await popoRequest<unknown>({
|
|
352
|
+
cfg: popoCfg,
|
|
353
|
+
method: "PUT",
|
|
354
|
+
path: "/open-apis/robots/v1/im/msg-card/stream",
|
|
355
|
+
body: {
|
|
356
|
+
instanceUuid,
|
|
357
|
+
templateUuid,
|
|
358
|
+
key: streamKey,
|
|
359
|
+
content,
|
|
360
|
+
sequence,
|
|
361
|
+
isFinalize,
|
|
362
|
+
isError,
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (response.errcode !== 0) {
|
|
367
|
+
return {
|
|
368
|
+
success: false,
|
|
369
|
+
error: response.errmsg || `errcode ${response.errcode}`,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return { success: true };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export type InstructionVariableOption = {
|
|
377
|
+
optionId: string;
|
|
378
|
+
optionValue: string;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
export type UpdateInstructionVariableOptionsParams = {
|
|
382
|
+
cfg: ClawdbotConfig;
|
|
383
|
+
instructionId: string;
|
|
384
|
+
variableKey: string;
|
|
385
|
+
variableOptions: InstructionVariableOption[];
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
export type UpdateInstructionVariableOptionsResult = {
|
|
389
|
+
success: boolean;
|
|
390
|
+
error?: string;
|
|
391
|
+
traceId?: string;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Update dynamic options for an instruction variable.
|
|
396
|
+
* This allows robots to dynamically update the options shown in slash commands.
|
|
397
|
+
*/
|
|
398
|
+
export async function updateInstructionVariableOptions(
|
|
399
|
+
params: UpdateInstructionVariableOptionsParams
|
|
400
|
+
): Promise<UpdateInstructionVariableOptionsResult> {
|
|
401
|
+
const { cfg, instructionId, variableKey, variableOptions } = params;
|
|
402
|
+
const popoCfg = cfg.channels?.popo as PopoConfig | undefined;
|
|
403
|
+
if (!popoCfg) {
|
|
404
|
+
throw new Error("POPO channel not configured");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const response = await popoRequest<{ traceId?: string }>({
|
|
408
|
+
cfg: popoCfg,
|
|
409
|
+
method: "POST",
|
|
410
|
+
path: "/open-apis/robots/v1/instruction/variable/options",
|
|
411
|
+
body: {
|
|
412
|
+
instructionId,
|
|
413
|
+
variableKey,
|
|
414
|
+
variableOptions,
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
if (response.errcode !== 0) {
|
|
419
|
+
return {
|
|
420
|
+
success: false,
|
|
421
|
+
error: response.errmsg || `errcode ${response.errcode}`,
|
|
422
|
+
traceId: response.data?.traceId,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
success: true,
|
|
428
|
+
traceId: response.data?.traceId,
|
|
429
|
+
};
|
|
430
|
+
}
|
package/src/targets.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type PopoReceiverType = "email" | "groupId";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize a POPO target string.
|
|
5
|
+
* Supports formats:
|
|
6
|
+
* - "user:email@example.com" -> "email@example.com"
|
|
7
|
+
* - "group:123456" -> "123456"
|
|
8
|
+
* - "email@example.com" -> "email@example.com"
|
|
9
|
+
* - "123456" -> "123456"
|
|
10
|
+
*/
|
|
11
|
+
export function normalizePopoTarget(raw: string): string | null {
|
|
12
|
+
const trimmed = raw.trim();
|
|
13
|
+
if (!trimmed) return null;
|
|
14
|
+
|
|
15
|
+
const lowered = trimmed.toLowerCase();
|
|
16
|
+
if (lowered.startsWith("user:")) {
|
|
17
|
+
return trimmed.slice("user:".length).trim() || null;
|
|
18
|
+
}
|
|
19
|
+
if (lowered.startsWith("group:")) {
|
|
20
|
+
return trimmed.slice("group:".length).trim() || null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return trimmed;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Detect if a target is an email (P2P) or group ID.
|
|
28
|
+
*/
|
|
29
|
+
export function detectReceiverType(target: string): PopoReceiverType {
|
|
30
|
+
// If it contains @ it's an email
|
|
31
|
+
if (target.includes("@")) {
|
|
32
|
+
return "email";
|
|
33
|
+
}
|
|
34
|
+
// Otherwise assume it's a group ID
|
|
35
|
+
return "groupId";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Format a POPO target for display.
|
|
40
|
+
*/
|
|
41
|
+
export function formatPopoTarget(id: string, type?: PopoReceiverType): string {
|
|
42
|
+
const trimmed = id.trim();
|
|
43
|
+
const actualType = type ?? detectReceiverType(trimmed);
|
|
44
|
+
|
|
45
|
+
if (actualType === "email") {
|
|
46
|
+
return `user:${trimmed}`;
|
|
47
|
+
}
|
|
48
|
+
return `group:${trimmed}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if a string looks like a POPO ID.
|
|
53
|
+
*/
|
|
54
|
+
export function looksLikePopoId(raw: string): boolean {
|
|
55
|
+
const trimmed = raw.trim();
|
|
56
|
+
if (!trimmed) return false;
|
|
57
|
+
|
|
58
|
+
// Has prefix
|
|
59
|
+
if (/^(user|group):/i.test(trimmed)) return true;
|
|
60
|
+
|
|
61
|
+
// Looks like an email
|
|
62
|
+
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) return true;
|
|
63
|
+
|
|
64
|
+
// Could be a group ID (numeric string)
|
|
65
|
+
if (/^\d+$/.test(trimmed)) return true;
|
|
66
|
+
|
|
67
|
+
return false;
|
|
68
|
+
}
|