wldx-cli 1.0.0 → 1.2.0
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/README.md +19 -22
- package/package.json +9 -6
- package/src/chat/format.mjs +40 -8
- package/src/chat/lib.mjs +71 -23
- package/src/chat/watch.mjs +46 -15
- package/src/cli.mjs +13 -252
- package/src/commands/calls.mjs +135 -32
- package/src/commands/channel.mjs +99 -21
- package/src/commands/chat.mjs +77 -22
- package/src/commands/conversations.mjs +146 -29
- package/src/commands/file.mjs +13 -5
- package/src/commands/inbox.mjs +20 -13
- package/src/commands/index.mjs +5 -29
- package/src/commands/live.mjs +82 -24
- package/src/commands/meetings.mjs +114 -27
- package/src/commands/phone.mjs +145 -33
- package/src/commands/reports.mjs +219 -44
- package/src/commands/section.mjs +49 -8
- package/src/commands/user.mjs +42 -13
- package/src/history/text.mjs +30 -4
- package/src/history/wda.mjs +20 -10
- package/src/live/socket.mjs +25 -7
- package/src/live/stats.mjs +41 -9
- package/src/live/text.mjs +122 -23
- package/src/live/views.mjs +93 -23
- package/src/phone/control.mjs +93 -33
- package/src/phone/events.mjs +38 -7
- package/src/phone/pbx.mjs +29 -11
- package/src/phone/sip.mjs +87 -17
- package/src/phone/xml.mjs +2 -1
- package/src/stream.mjs +10 -5
- package/src/version.mjs +0 -1
- package/src/auth.mjs +0 -274
- package/src/commands/login.mjs +0 -126
- package/src/commands/profile.mjs +0 -51
- package/src/commands/status.mjs +0 -95
- package/src/completion.mjs +0 -132
- package/src/errors.mjs +0 -30
- package/src/files.mjs +0 -98
- package/src/format.mjs +0 -82
- package/src/http.mjs +0 -64
- package/src/services.mjs +0 -58
package/src/commands/reports.mjs
CHANGED
|
@@ -2,29 +2,77 @@
|
|
|
2
2
|
// x-caracal service of WDA, and the list of the queues. Read only. Counts, never percentages from the service: a
|
|
3
3
|
// rate is computed from the counts (in the text only). A report covers one PBX: the one named by --pbx, else the
|
|
4
4
|
// PBX of the first --queue (its id is <company>_<pbx>_<number>), else the PBX of the login.
|
|
5
|
-
import {UsageError} from '
|
|
6
|
-
import {localTime, oneLine, table} from '
|
|
7
|
-
import {apiPath, query} from '
|
|
5
|
+
import {UsageError} from 'wldx-core/errors';
|
|
6
|
+
import {localTime, oneLine, table} from 'wldx-core/format';
|
|
7
|
+
import {apiPath, query} from 'wldx-core/http';
|
|
8
8
|
import {millis} from '../history/text.mjs';
|
|
9
9
|
import {callFlow, checkId, checkZone, intOption, plural, wdaClient} from '../history/wda.mjs';
|
|
10
10
|
|
|
11
|
-
export const OUTCOMES = [
|
|
11
|
+
export const OUTCOMES = [
|
|
12
|
+
'offered',
|
|
13
|
+
'received',
|
|
14
|
+
'answered',
|
|
15
|
+
'completed',
|
|
16
|
+
'transferred',
|
|
17
|
+
'abandoned',
|
|
18
|
+
'unanswered',
|
|
19
|
+
'lost',
|
|
20
|
+
'lostAbandoned',
|
|
21
|
+
'lostUnanswered',
|
|
22
|
+
'overflow',
|
|
23
|
+
'unknown',
|
|
24
|
+
];
|
|
12
25
|
|
|
13
26
|
const FILTER_OPTIONS = {
|
|
14
|
-
from: {
|
|
27
|
+
from: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
placeholder: 'YYYY-MM-DD',
|
|
30
|
+
description: 'the first day (today when --from and --to are left out)',
|
|
31
|
+
},
|
|
15
32
|
to: {type: 'string', placeholder: 'YYYY-MM-DD', description: 'the last day, inclusive (today by default)'},
|
|
16
33
|
days: {type: 'string', placeholder: 'N', description: 'the last N days, today included (1-90)'},
|
|
17
34
|
tz: {type: 'string', placeholder: 'ZONE', description: "the zone the days are cut in (the PBX's own by default)"},
|
|
18
|
-
pbx: {
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
pbx: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
placeholder: 'SERIAL',
|
|
38
|
+
description: 'the PBX of the report (else the PBX of the first --queue, else of the login)',
|
|
39
|
+
},
|
|
40
|
+
queue: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
multiple: true,
|
|
43
|
+
placeholder: 'ID',
|
|
44
|
+
description: 'only these queues (ids from wx reports list)',
|
|
45
|
+
},
|
|
46
|
+
extension: {
|
|
47
|
+
type: 'string',
|
|
48
|
+
multiple: true,
|
|
49
|
+
placeholder: 'EXT',
|
|
50
|
+
description: 'only the calls these users answered, or that rang them unanswered',
|
|
51
|
+
},
|
|
21
52
|
outcome: {type: 'string', multiple: true, choices: OUTCOMES, description: 'only calls of these outcomes'},
|
|
22
|
-
hours: {
|
|
53
|
+
hours: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
placeholder: 'HH:MM-HH:MM',
|
|
56
|
+
description: 'the time of day the call entered the queue (slow: reads every call)',
|
|
57
|
+
},
|
|
23
58
|
weekday: {type: 'string', multiple: true, placeholder: '1-7', description: '1 = Monday … 7 = Sunday (slow)'},
|
|
24
|
-
wait: {
|
|
59
|
+
wait: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
placeholder: 'FROM-TO',
|
|
62
|
+
description: 'a band of wait seconds: more than FROM, at most TO (TO may be left out) (slow)',
|
|
63
|
+
},
|
|
25
64
|
talk: {type: 'string', placeholder: 'FROM-TO', description: 'a band of talk seconds (slow)'},
|
|
26
|
-
reason: {
|
|
27
|
-
|
|
65
|
+
reason: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
multiple: true,
|
|
68
|
+
placeholder: 'R',
|
|
69
|
+
description: 'disconnect reasons: HANGUP, TRANSFER, TIMEOUT, EMPTY, EXITWITHKEY, … (slow)',
|
|
70
|
+
},
|
|
71
|
+
sla: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
placeholder: 'S',
|
|
74
|
+
description: 'the SLA threshold in seconds, 5 to 120 in steps of 5 (20 by default)',
|
|
75
|
+
},
|
|
28
76
|
'queue-sla': {type: 'string', multiple: true, placeholder: 'ID=S', description: 'the SLA threshold of one queue'},
|
|
29
77
|
};
|
|
30
78
|
|
|
@@ -44,7 +92,8 @@ const localDay = (now, back) => {
|
|
|
44
92
|
function checkDay(name, value) {
|
|
45
93
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
46
94
|
const d = m && new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
|
|
47
|
-
if (!m || d.getUTCMonth() !== +m[2] - 1 || d.getUTCDate() !== +m[3])
|
|
95
|
+
if (!m || d.getUTCMonth() !== +m[2] - 1 || d.getUTCDate() !== +m[3])
|
|
96
|
+
throw new UsageError(`${name} must be a day as YYYY-MM-DD: ${value}`);
|
|
48
97
|
return value;
|
|
49
98
|
}
|
|
50
99
|
|
|
@@ -100,7 +149,8 @@ export function reportFilter(v, now) {
|
|
|
100
149
|
f.disconnectReasons = v.reason;
|
|
101
150
|
}
|
|
102
151
|
if (v.weekday?.length) {
|
|
103
|
-
for (const d of v.weekday)
|
|
152
|
+
for (const d of v.weekday)
|
|
153
|
+
if (!/^[1-7]$/.test(d)) throw new UsageError(`--weekday is 1 (Monday) to 7 (Sunday): ${d}`);
|
|
104
154
|
f.daysOfWeek = v.weekday.map(Number);
|
|
105
155
|
}
|
|
106
156
|
if (v.hours !== undefined) {
|
|
@@ -142,16 +192,30 @@ function ladder(v, widthKey, stepsKey) {
|
|
|
142
192
|
// The queues: {total, pbxs: {pbx: n}, queues: [{id, name, pbx, aclGroup?}]}, by PBX and name.
|
|
143
193
|
export function shapeQueues(r, {pbx, name} = {}) {
|
|
144
194
|
const needle = (name ?? '').toLowerCase();
|
|
145
|
-
const list = (r.services ?? []).filter(
|
|
195
|
+
const list = (r.services ?? []).filter(
|
|
196
|
+
(s) =>
|
|
197
|
+
(!pbx || s.pbx === pbx) &&
|
|
198
|
+
(!needle ||
|
|
199
|
+
String(s.name ?? '')
|
|
200
|
+
.toLowerCase()
|
|
201
|
+
.includes(needle)),
|
|
202
|
+
);
|
|
146
203
|
const pbxs = {};
|
|
147
204
|
for (const s of [...list].sort((a, b) => cmp(a.pbx, b.pbx))) pbxs[s.pbx] = (pbxs[s.pbx] ?? 0) + 1;
|
|
148
205
|
const queues = [...list]
|
|
149
206
|
.sort((a, b) => cmp(a.pbx, b.pbx) || cmp(a.name ?? '', b.name ?? ''))
|
|
150
|
-
.map((s) =>
|
|
207
|
+
.map((s) =>
|
|
208
|
+
Object.fromEntries(
|
|
209
|
+
Object.entries({id: s.id, name: s.name, pbx: s.pbx, aclGroup: s.aclGroup ? s.aclGroup : null}).filter(
|
|
210
|
+
([, x]) => x !== null && x !== undefined,
|
|
211
|
+
),
|
|
212
|
+
),
|
|
213
|
+
);
|
|
151
214
|
return {total: list.length, pbxs, queues};
|
|
152
215
|
}
|
|
153
216
|
|
|
154
|
-
const cmp = (a, b) =>
|
|
217
|
+
const cmp = (a, b) =>
|
|
218
|
+
a === b ? 0 : a === undefined || a === null ? -1 : b === undefined || b === null ? 1 : a < b ? -1 : 1;
|
|
155
219
|
|
|
156
220
|
// Customers' numbers and names are shown only with --full.
|
|
157
221
|
const CALLER = ['callerPhone', 'callerName'];
|
|
@@ -160,8 +224,10 @@ const dropCaller = (row) => (row ? Object.fromEntries(Object.entries(row).filter
|
|
|
160
224
|
// Text.
|
|
161
225
|
|
|
162
226
|
const n = (x) => (Number.isFinite(x) ? String(x) : '');
|
|
163
|
-
const rate = (within, eligible) =>
|
|
164
|
-
|
|
227
|
+
const rate = (within, eligible) =>
|
|
228
|
+
eligible ? `${within}/${eligible} ${Math.round((100 * within) / eligible)}%` : 'no calls';
|
|
229
|
+
const slaText = (s) =>
|
|
230
|
+
s ? `${rate(s.answeredWithin ?? s.abandonedWithin ?? 0, s.eligible ?? 0)} within ${s.thresholdSeconds ?? 20} s` : '';
|
|
165
231
|
|
|
166
232
|
function heading(r) {
|
|
167
233
|
const w = r.window;
|
|
@@ -174,7 +240,10 @@ function metricsRows(m = {}) {
|
|
|
174
240
|
return [
|
|
175
241
|
['Offered', n(c.offered)],
|
|
176
242
|
['Received', n(c.received)],
|
|
177
|
-
[
|
|
243
|
+
[
|
|
244
|
+
'Answered',
|
|
245
|
+
`${(c.completed ?? 0) + (c.transferred ?? 0)} (completed ${c.completed ?? 0}, transferred ${c.transferred ?? 0})`,
|
|
246
|
+
],
|
|
178
247
|
['Abandoned', `${c.abandoned ?? 0}${c.shortAbandoned ? ` (within 5 s: ${c.shortAbandoned})` : ''}`],
|
|
179
248
|
['Unanswered', n(c.unanswered ?? 0)],
|
|
180
249
|
['Lost', `${c.lost ?? 0} (abandoned ${c.lostAbandoned ?? 0}, unanswered ${c.lostUnanswered ?? 0})`],
|
|
@@ -191,12 +260,25 @@ function metricsRows(m = {}) {
|
|
|
191
260
|
const METRIC_HEAD = ['Offered', 'Answered', 'Abandoned', 'Lost', 'Overflow', 'SLA', 'Avg wait', 'Max wait', 'Avg talk'];
|
|
192
261
|
const metricCells = (m = {}) => {
|
|
193
262
|
const c = m.counts ?? {};
|
|
194
|
-
return [
|
|
263
|
+
return [
|
|
264
|
+
n(c.offered ?? 0),
|
|
265
|
+
n((c.completed ?? 0) + (c.transferred ?? 0)),
|
|
266
|
+
n(c.abandoned ?? 0),
|
|
267
|
+
n(c.lost ?? 0),
|
|
268
|
+
n(c.overflow ?? 0),
|
|
269
|
+
m.sla ? rate(m.sla.answeredWithin ?? 0, m.sla.eligible ?? 0) : '',
|
|
270
|
+
millis(m.avgWaitMs),
|
|
271
|
+
millis(m.maxWaitMs),
|
|
272
|
+
millis(m.avgTalkMs),
|
|
273
|
+
];
|
|
195
274
|
};
|
|
196
275
|
|
|
197
276
|
// 1 = Monday … 7 = Sunday, as in the --weekday filter.
|
|
198
277
|
const ISO_WEEKDAYS = [undefined, 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
|
199
|
-
const periodLabel = (b, granularity) =>
|
|
278
|
+
const periodLabel = (b, granularity) =>
|
|
279
|
+
granularity === 'dayOfWeek'
|
|
280
|
+
? (ISO_WEEKDAYS[Number(b.bucketKey)] ?? b.bucketKey)
|
|
281
|
+
: (b.bucketKey ?? b.bucketStart ?? '');
|
|
200
282
|
|
|
201
283
|
const who = (name, ext) => [name, ext && `(${ext})`].filter(Boolean).join(' ');
|
|
202
284
|
|
|
@@ -247,7 +329,13 @@ const group = {
|
|
|
247
329
|
return shapeQueues(await ctx.client(wdaClient).post('/stream/services', {}), {pbx, name});
|
|
248
330
|
},
|
|
249
331
|
text(r) {
|
|
250
|
-
const lines = [
|
|
332
|
+
const lines = [
|
|
333
|
+
`${plural(r.total, 'queue')} · ${
|
|
334
|
+
Object.entries(r.pbxs)
|
|
335
|
+
.map(([p, k]) => `PBX ${p}: ${k}`)
|
|
336
|
+
.join(', ') || 'no PBX'
|
|
337
|
+
}`,
|
|
338
|
+
];
|
|
251
339
|
if (r.queues.length) lines.push(...table(r.queues.map((q) => [q.pbx ?? '', oneLine(q.name ?? '', 50), q.id])));
|
|
252
340
|
return lines;
|
|
253
341
|
},
|
|
@@ -255,7 +343,8 @@ const group = {
|
|
|
255
343
|
summary: {
|
|
256
344
|
summary: 'every queue call of the filter: offered, answered, abandoned, lost, waits, talk, ring, the SLA counts',
|
|
257
345
|
usage: 'wx reports summary [filter]',
|
|
258
|
-
details:
|
|
346
|
+
details:
|
|
347
|
+
'Filter: --from/--to days or --days N (today by default; at most 90 days), --tz, --pbx, --queue, --extension, --outcome, --sla, --queue-sla; slow ones: --hours, --weekday, --wait, --talk, --reason.',
|
|
259
348
|
options: FILTER_OPTIONS,
|
|
260
349
|
args: [0, 0],
|
|
261
350
|
run: (ctx) => report(ctx, 'distribution/summary/query'),
|
|
@@ -267,38 +356,81 @@ const group = {
|
|
|
267
356
|
options: FILTER_OPTIONS,
|
|
268
357
|
args: [0, 0],
|
|
269
358
|
run: (ctx) => report(ctx, 'distribution/by-service/query'),
|
|
270
|
-
text: (r) => [
|
|
359
|
+
text: (r) => [
|
|
360
|
+
heading(r),
|
|
361
|
+
...table([
|
|
362
|
+
['Queue', ...METRIC_HEAD],
|
|
363
|
+
...(r.groups ?? []).map((g) => [oneLine(g.serviceName ?? g.serviceId ?? '', 40), ...metricCells(g.metrics)]),
|
|
364
|
+
]),
|
|
365
|
+
],
|
|
271
366
|
},
|
|
272
367
|
'by-period': {
|
|
273
368
|
summary: 'the same figures as a series (hour: the 24 hours of the day over the window)',
|
|
274
369
|
usage: 'wx reports by-period --by hour|day|week|month|weekday [filter]',
|
|
275
|
-
options: {
|
|
370
|
+
options: {
|
|
371
|
+
...FILTER_OPTIONS,
|
|
372
|
+
by: {
|
|
373
|
+
type: 'string',
|
|
374
|
+
choices: ['hour', 'day', 'week', 'month', 'weekday'],
|
|
375
|
+
description: 'the period of a bucket',
|
|
376
|
+
},
|
|
377
|
+
},
|
|
276
378
|
args: [0, 0],
|
|
277
379
|
run(ctx) {
|
|
278
380
|
if (!ctx.values.by) throw new UsageError('by-period needs --by hour|day|week|month|weekday', ctx.usage());
|
|
279
|
-
return report(ctx, 'distribution/by-period/query', {
|
|
381
|
+
return report(ctx, 'distribution/by-period/query', {
|
|
382
|
+
granularity: ctx.values.by === 'weekday' ? 'dayOfWeek' : ctx.values.by,
|
|
383
|
+
});
|
|
280
384
|
},
|
|
281
|
-
text: (r) => [
|
|
385
|
+
text: (r) => [
|
|
386
|
+
heading(r),
|
|
387
|
+
...table([
|
|
388
|
+
['Period', ...METRIC_HEAD],
|
|
389
|
+
...(r.buckets ?? []).map((b) => [periodLabel(b, r.granularity), ...metricCells(b.metrics)]),
|
|
390
|
+
]),
|
|
391
|
+
],
|
|
282
392
|
},
|
|
283
393
|
answered: {
|
|
284
394
|
summary: 'the answered calls: summary, or grouped by queue, agent, user, reason or talk time',
|
|
285
395
|
usage: 'wx reports answered [--by queue|agent|user|reason|talk-time] [--band S --steps N] [filter]',
|
|
286
|
-
details:
|
|
287
|
-
|
|
396
|
+
details:
|
|
397
|
+
'agent: a person in one queue; user: a person over every queue; talk-time: bands of --band seconds, --steps of them. The breakdowns read every call of the window (slow).',
|
|
398
|
+
options: {
|
|
399
|
+
...FILTER_OPTIONS,
|
|
400
|
+
by: {
|
|
401
|
+
type: 'string',
|
|
402
|
+
choices: ['queue', 'agent', 'user', 'reason', 'talk-time'],
|
|
403
|
+
description: 'group the calls',
|
|
404
|
+
},
|
|
405
|
+
...LADDER,
|
|
406
|
+
},
|
|
288
407
|
args: [0, 0],
|
|
289
408
|
run(ctx) {
|
|
290
409
|
const v = ctx.values;
|
|
291
|
-
if (v.by !== 'talk-time' && (v.band !== undefined || v.steps !== undefined))
|
|
410
|
+
if (v.by !== 'talk-time' && (v.band !== undefined || v.steps !== undefined))
|
|
411
|
+
throw new UsageError('--band and --steps go with --by talk-time');
|
|
292
412
|
if (!v.by) return report(ctx, 'answered/summary/query');
|
|
293
|
-
const dimension = {
|
|
294
|
-
|
|
413
|
+
const dimension = {
|
|
414
|
+
queue: 'byService',
|
|
415
|
+
agent: 'byAgent',
|
|
416
|
+
user: 'byUser',
|
|
417
|
+
reason: 'byDisconnectReason',
|
|
418
|
+
'talk-time': 'byTalkTime',
|
|
419
|
+
}[v.by];
|
|
420
|
+
return report(ctx, 'answered/breakdown/query', {
|
|
421
|
+
...(dimension === 'byTalkTime' ? ladder(v, 'talkBandSeconds', 'talkBandSteps') : {}),
|
|
422
|
+
dimension,
|
|
423
|
+
});
|
|
295
424
|
},
|
|
296
425
|
text(r) {
|
|
297
426
|
if (!r.groups) {
|
|
298
427
|
return [
|
|
299
428
|
heading(r),
|
|
300
429
|
...table([
|
|
301
|
-
[
|
|
430
|
+
[
|
|
431
|
+
'Answered',
|
|
432
|
+
`${(r.completed ?? 0) + (r.transferred ?? 0)} (completed ${r.completed ?? 0}, transferred ${r.transferred ?? 0})`,
|
|
433
|
+
],
|
|
302
434
|
...(r.unknown ? [['Unknown', n(r.unknown)]] : []),
|
|
303
435
|
['Service level', slaText(r.sla)],
|
|
304
436
|
['Wait', `avg ${millis(r.avgWaitMs)}, max ${millis(r.maxWaitMs)}`],
|
|
@@ -308,7 +440,18 @@ const group = {
|
|
|
308
440
|
}
|
|
309
441
|
return [
|
|
310
442
|
`${heading(r)} · ${r.total ?? 0} answered`,
|
|
311
|
-
...table([
|
|
443
|
+
...table([
|
|
444
|
+
['', 'Calls', 'Completed', 'Transferred', 'Avg wait', 'Avg talk', 'Total talk'],
|
|
445
|
+
...r.groups.map((g) => [
|
|
446
|
+
oneLine(groupKey(g, r.dimension), 60),
|
|
447
|
+
n(g.calls ?? 0),
|
|
448
|
+
n(g.completed ?? 0),
|
|
449
|
+
n(g.transferred ?? 0),
|
|
450
|
+
millis(g.avgWaitMs),
|
|
451
|
+
millis(g.avgTalkMs),
|
|
452
|
+
millis(g.totalTalkMs),
|
|
453
|
+
]),
|
|
454
|
+
]),
|
|
312
455
|
];
|
|
313
456
|
},
|
|
314
457
|
},
|
|
@@ -319,7 +462,9 @@ const group = {
|
|
|
319
462
|
args: [0, 0],
|
|
320
463
|
run(ctx) {
|
|
321
464
|
if (!ctx.values.by) return report(ctx, 'unanswered/summary/query');
|
|
322
|
-
return report(ctx, 'unanswered/breakdown/query', {
|
|
465
|
+
return report(ctx, 'unanswered/breakdown/query', {
|
|
466
|
+
dimension: ctx.values.by === 'queue' ? 'byService' : 'byDisconnectReason',
|
|
467
|
+
});
|
|
323
468
|
},
|
|
324
469
|
text(r) {
|
|
325
470
|
if (!r.groups) {
|
|
@@ -332,14 +477,28 @@ const group = {
|
|
|
332
477
|
['Overflow', n(r.overflow ?? 0)],
|
|
333
478
|
...(r.unknown ? [['Unknown', n(r.unknown)]] : []),
|
|
334
479
|
['Given up', slaText(r.sla)],
|
|
335
|
-
[
|
|
480
|
+
[
|
|
481
|
+
'Wait',
|
|
482
|
+
`avg ${millis(r.avgWaitMs)}, max ${millis(r.maxWaitMs)}, abandoned avg ${millis(r.abandonedAvgWaitMs)}`,
|
|
483
|
+
],
|
|
336
484
|
['Place in line', `avg start ${n(r.avgStartPosition ?? 0)}, avg final ${n(r.avgFinalPosition ?? 0)}`],
|
|
337
485
|
]),
|
|
338
486
|
];
|
|
339
487
|
}
|
|
340
488
|
return [
|
|
341
489
|
`${heading(r)} · ${r.total ?? 0} not answered`,
|
|
342
|
-
...table([
|
|
490
|
+
...table([
|
|
491
|
+
['', 'Abandoned', 'Unanswered', 'Lost', 'Overflow', 'Avg wait', 'Max wait'],
|
|
492
|
+
...r.groups.map((g) => [
|
|
493
|
+
oneLine(groupKey(g, r.dimension), 60),
|
|
494
|
+
n(g.abandoned ?? 0),
|
|
495
|
+
n(g.unanswered ?? 0),
|
|
496
|
+
n(g.lost ?? 0),
|
|
497
|
+
n(g.overflow ?? 0),
|
|
498
|
+
millis(g.avgWaitMs),
|
|
499
|
+
millis(g.maxWaitMs),
|
|
500
|
+
]),
|
|
501
|
+
]),
|
|
343
502
|
];
|
|
344
503
|
},
|
|
345
504
|
},
|
|
@@ -350,20 +509,27 @@ const group = {
|
|
|
350
509
|
args: [1, 1],
|
|
351
510
|
run(ctx) {
|
|
352
511
|
const kind = ctx.positionals[0];
|
|
353
|
-
if (kind !== 'answered' && kind !== 'unanswered')
|
|
512
|
+
if (kind !== 'answered' && kind !== 'unanswered')
|
|
513
|
+
throw new UsageError('service-level answered|unanswered', ctx.usage());
|
|
354
514
|
return report(ctx, `${kind}/service-level/query`, ladder(ctx.values, 'waitBandSeconds', 'waitBandSteps'));
|
|
355
515
|
},
|
|
356
516
|
text(r, ctx) {
|
|
357
517
|
const unanswered = ctx.positionals[0] === 'unanswered';
|
|
358
518
|
const head = `${heading(r)} · ${r.total ?? 0} ${unanswered ? 'not answered' : 'answered'}${unanswered ? ` (lost ${r.lost ?? 0}, overflow ${r.overflow ?? 0})` : ''}`;
|
|
359
|
-
const rows = (r.bands ?? []).map((b) => [
|
|
519
|
+
const rows = (r.bands ?? []).map((b) => [
|
|
520
|
+
`within ${b.withinSeconds} s`,
|
|
521
|
+
n(b.count),
|
|
522
|
+
`+${b.delta ?? 0}`,
|
|
523
|
+
...(unanswered ? [`lost ${b.lost ?? 0}`, `overflow ${b.overflow ?? 0}`] : []),
|
|
524
|
+
]);
|
|
360
525
|
return [head, ...table(rows)];
|
|
361
526
|
},
|
|
362
527
|
},
|
|
363
528
|
calls: {
|
|
364
529
|
summary: 'the queue calls one by one, newest first: queue, times, wait, talk, outcome, who answered',
|
|
365
530
|
usage: 'wx reports calls [--limit N] [--pages N] [--cursor C] [--call CALLID] [--full] [filter]',
|
|
366
|
-
details:
|
|
531
|
+
details:
|
|
532
|
+
'50 a page (--limit up to 200); --pages follows the next pages (up to 20); --call: every queue a phone call went through. The caller (number and name) only with --full: show it when the user asks for the callers.',
|
|
367
533
|
options: {
|
|
368
534
|
...FILTER_OPTIONS,
|
|
369
535
|
limit: {type: 'string', placeholder: 'N', description: 'calls per page (1-200, 50 by default)'},
|
|
@@ -377,7 +543,8 @@ const group = {
|
|
|
377
543
|
const v = ctx.values;
|
|
378
544
|
const limit = intOption('--limit', v.limit, 1, 200);
|
|
379
545
|
const pages = intOption('--pages', v.pages, 1, 20) ?? 1;
|
|
380
|
-
if (v.cursor !== undefined && (v.cursor.length > 2048 || !/^[A-Za-z0-9_=+/.:-]+$/.test(v.cursor)))
|
|
546
|
+
if (v.cursor !== undefined && (v.cursor.length > 2048 || !/^[A-Za-z0-9_=+/.:-]+$/.test(v.cursor)))
|
|
547
|
+
throw new UsageError('not a cursor');
|
|
381
548
|
const filter = reportFilter(v, ctx.now);
|
|
382
549
|
const x = {};
|
|
383
550
|
if (limit !== undefined) x.limit = limit;
|
|
@@ -416,12 +583,20 @@ const group = {
|
|
|
416
583
|
async run(ctx) {
|
|
417
584
|
const {callId, flow} = callFlow(ctx.positionals);
|
|
418
585
|
const q = ctx.values.queue === undefined ? undefined : checkId('the queue id', ctx.values.queue);
|
|
419
|
-
const r = await ctx
|
|
586
|
+
const r = await ctx
|
|
587
|
+
.client(wdaClient)
|
|
588
|
+
.get(apiPath`/xcaracal/services/calls/${callId}/flows/${String(flow)}/events` + query({serviceId: q}));
|
|
420
589
|
return ctx.values.full ? r : {...r, call: dropCaller(r.call)};
|
|
421
590
|
},
|
|
422
591
|
text(r, ctx) {
|
|
423
592
|
const lines = r.call ? table([callLine(r.call, ctx.values.full)]) : [];
|
|
424
|
-
const rows = (r.events ?? []).map((e) => [
|
|
593
|
+
const rows = (r.events ?? []).map((e) => [
|
|
594
|
+
localTime(e.time, {seconds: true}),
|
|
595
|
+
e.action ?? '',
|
|
596
|
+
e.reason ?? '',
|
|
597
|
+
who(e.userName, e.userExtension),
|
|
598
|
+
e.queuePosition === undefined ? '' : `place ${e.queuePosition}`,
|
|
599
|
+
]);
|
|
425
600
|
return [...lines, ...(rows.length ? table(rows).map((l) => ` ${l}`) : [' No events.'])];
|
|
426
601
|
},
|
|
427
602
|
},
|
package/src/commands/section.mjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// wx section: the user's sections of the chat inbox (the agent skill's sections.sh). The list and its counters come
|
|
2
2
|
// from the inbox; creating, renaming, deleting and moving channels are not implemented by the service yet.
|
|
3
|
-
import {UsageError} from '
|
|
4
|
-
import {apiPath} from '
|
|
3
|
+
import {UsageError} from 'wldx-core/errors';
|
|
4
|
+
import {apiPath} from 'wldx-core/http';
|
|
5
5
|
import {channelId} from '../stream.mjs';
|
|
6
6
|
import {done} from '../chat/format.mjs';
|
|
7
7
|
import {NOT_IMPLEMENTED, api, notImplemented} from '../chat/lib.mjs';
|
|
8
8
|
import {sectionLines} from './inbox.mjs';
|
|
9
9
|
|
|
10
10
|
function sectionId(value) {
|
|
11
|
-
if (!/^[a-z0-9_-]{1,32}$/.test(String(value)))
|
|
11
|
+
if (!/^[a-z0-9_-]{1,32}$/.test(String(value)))
|
|
12
|
+
throw new UsageError(`A section id must match [a-z0-9_-] (up to 32): ${value}`);
|
|
12
13
|
return value;
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -49,11 +50,51 @@ const section = {
|
|
|
49
50
|
auth: true,
|
|
50
51
|
commands: {
|
|
51
52
|
list,
|
|
52
|
-
create: make(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
create: make(
|
|
54
|
+
'create',
|
|
55
|
+
'create a section',
|
|
56
|
+
'<sectionId> <name>',
|
|
57
|
+
'CreateSection',
|
|
58
|
+
2,
|
|
59
|
+
([id, name]) => ['POST', '/sections', {sectionId: sectionId(id), name: sectionName(name)}],
|
|
60
|
+
'Created',
|
|
61
|
+
),
|
|
62
|
+
rename: make(
|
|
63
|
+
'rename',
|
|
64
|
+
'rename a section',
|
|
65
|
+
'<sectionId> <name>',
|
|
66
|
+
'RenameSection',
|
|
67
|
+
2,
|
|
68
|
+
([id, name]) => ['PATCH', apiPath`/sections/${sectionId(id)}`, {name: sectionName(name)}],
|
|
69
|
+
'Renamed',
|
|
70
|
+
),
|
|
71
|
+
delete: make(
|
|
72
|
+
'delete',
|
|
73
|
+
'delete a section',
|
|
74
|
+
'<sectionId>',
|
|
75
|
+
'DeleteSection',
|
|
76
|
+
1,
|
|
77
|
+
([id]) => ['DELETE', apiPath`/sections/${sectionId(id)}`],
|
|
78
|
+
'Deleted',
|
|
79
|
+
),
|
|
80
|
+
add: make(
|
|
81
|
+
'add',
|
|
82
|
+
'put a channel into a section',
|
|
83
|
+
'<channel> <sectionId>',
|
|
84
|
+
'MoveChannelToSection',
|
|
85
|
+
2,
|
|
86
|
+
([ch, id]) => ['PUT', apiPath`/channels/${channelId(ch)}/sections/${sectionId(id)}`],
|
|
87
|
+
'Added',
|
|
88
|
+
),
|
|
89
|
+
remove: make(
|
|
90
|
+
'remove',
|
|
91
|
+
'take a channel out of a section',
|
|
92
|
+
'<channel> <sectionId>',
|
|
93
|
+
'RemoveChannelFromSection',
|
|
94
|
+
2,
|
|
95
|
+
([ch, id]) => ['DELETE', apiPath`/channels/${channelId(ch)}/sections/${sectionId(id)}`],
|
|
96
|
+
'Removed',
|
|
97
|
+
),
|
|
57
98
|
},
|
|
58
99
|
};
|
|
59
100
|
|
package/src/commands/user.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// wx user: people of the corporate directory on the stream service (the agent skill's users.sh and search.sh
|
|
2
2
|
// users): who you are, a colleague by name (any script, typos, as it sounds), e-mail or phone.
|
|
3
|
-
import {jwtClaims} from '
|
|
4
|
-
import {UsageError} from '
|
|
5
|
-
import {paint, table} from '
|
|
6
|
-
import {query} from '
|
|
3
|
+
import {jwtClaims} from 'wldx-core/auth';
|
|
4
|
+
import {UsageError} from 'wldx-core/errors';
|
|
5
|
+
import {paint, table} from 'wldx-core/format';
|
|
6
|
+
import {query} from 'wldx-core/http';
|
|
7
7
|
import {nextPage, userLine} from '../chat/format.mjs';
|
|
8
8
|
import {api, checkId, checkInt, displayName} from '../chat/lib.mjs';
|
|
9
9
|
|
|
@@ -20,7 +20,11 @@ const me = {
|
|
|
20
20
|
},
|
|
21
21
|
text(u, ctx) {
|
|
22
22
|
if (!u) return 'Not found in the chat directory.';
|
|
23
|
-
const rows = [
|
|
23
|
+
const rows = [
|
|
24
|
+
['Name', paint(ctx).bold(displayName(u) ?? '')],
|
|
25
|
+
['Id', u.id],
|
|
26
|
+
['E-mail', u.email ?? ''],
|
|
27
|
+
];
|
|
24
28
|
if (u.pbxExtension) rows.push(['Extension', u.pbxExtension]);
|
|
25
29
|
if (u.timeZone) rows.push(['Time zone', u.timeZone]);
|
|
26
30
|
if (u.locale) rows.push(['Locale', u.locale]);
|
|
@@ -31,12 +35,21 @@ const me = {
|
|
|
31
35
|
const brief = (r) => ({
|
|
32
36
|
margin: r?.margin,
|
|
33
37
|
nextCursor: r?.nextCursor,
|
|
34
|
-
results: (r?.results ?? []).map((x) => ({
|
|
38
|
+
results: (r?.results ?? []).map((x) => ({
|
|
39
|
+
id: x.user?.id,
|
|
40
|
+
name: x.user?.displayName,
|
|
41
|
+
email: x.user?.email,
|
|
42
|
+
extension: x.user?.pbxExtension,
|
|
43
|
+
bot: x.user?.bot,
|
|
44
|
+
confidence: x.confidence,
|
|
45
|
+
matchedOn: x.matchedOn,
|
|
46
|
+
})),
|
|
35
47
|
});
|
|
36
48
|
|
|
37
49
|
const find = {
|
|
38
50
|
summary: 'find a colleague by name (any script, typos, as it sounds), e-mail or phone',
|
|
39
|
-
usage:
|
|
51
|
+
usage:
|
|
52
|
+
'wx user find <query> [--mode auto|name|email|phone] [--kind human|bot|all] [--with-extension] [--min-confidence 0..1] [--brief] [--size 1-50] [--cursor C]',
|
|
40
53
|
details: [
|
|
41
54
|
'An e-mail or a phone number (with its country code, +…) is looked up exactly (confidence 1). confidence (0..1)',
|
|
42
55
|
'is how much the query looks like the name, not the probability that it is the right person (default cut 0.5;',
|
|
@@ -45,10 +58,18 @@ const find = {
|
|
|
45
58
|
'matchedOn (JSON).',
|
|
46
59
|
].join('\n'),
|
|
47
60
|
options: {
|
|
48
|
-
mode: {
|
|
61
|
+
mode: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
choices: ['auto', 'name', 'email', 'phone'],
|
|
64
|
+
description: 'what the query is (auto by default)',
|
|
65
|
+
},
|
|
49
66
|
kind: {type: 'string', choices: ['human', 'bot', 'all'], description: 'people (default), bots or both'},
|
|
50
67
|
'with-extension': {type: 'boolean', description: 'only people with a PBX extension'},
|
|
51
|
-
'min-confidence': {
|
|
68
|
+
'min-confidence': {
|
|
69
|
+
type: 'string',
|
|
70
|
+
placeholder: '0..1',
|
|
71
|
+
description: 'drop hits below this confidence (default 0.5)',
|
|
72
|
+
},
|
|
52
73
|
brief: {type: 'boolean', description: 'only the fields needed to pick a person'},
|
|
53
74
|
size: {type: 'string', placeholder: '1-50', description: 'results per page (default 20)'},
|
|
54
75
|
cursor: {type: 'string', description: 'the next page (nextCursor of the previous one)'},
|
|
@@ -57,7 +78,8 @@ const find = {
|
|
|
57
78
|
async run(ctx) {
|
|
58
79
|
const {values} = ctx;
|
|
59
80
|
const minc = values['min-confidence'];
|
|
60
|
-
if (minc !== undefined && !/^(0|1|0?\.[0-9]+|1\.0+)$/.test(minc))
|
|
81
|
+
if (minc !== undefined && !/^(0|1|0?\.[0-9]+|1\.0+)$/.test(minc))
|
|
82
|
+
throw new UsageError(`--min-confidence is a number from 0 to 1: ${minc}`);
|
|
61
83
|
const q = query({
|
|
62
84
|
q: ctx.positionals[0],
|
|
63
85
|
mode: values.mode,
|
|
@@ -79,7 +101,12 @@ const find = {
|
|
|
79
101
|
return [...userLine(u, c), `${Number(x.confidence ?? 0).toFixed(2)} ${x.matchedOn ?? ''}`.trim()];
|
|
80
102
|
});
|
|
81
103
|
const lines = table(rows);
|
|
82
|
-
if (hits.length > 1 && typeof r.margin === 'number')
|
|
104
|
+
if (hits.length > 1 && typeof r.margin === 'number')
|
|
105
|
+
lines.push(
|
|
106
|
+
r.margin < 0.1
|
|
107
|
+
? c.yellow(`Ambiguous (margin ${r.margin.toFixed(2)}): several people fit, ask which one`)
|
|
108
|
+
: c.dim(`margin ${r.margin.toFixed(2)}`),
|
|
109
|
+
);
|
|
83
110
|
return [...lines, ...nextPage(r.nextCursor, c)];
|
|
84
111
|
},
|
|
85
112
|
};
|
|
@@ -90,7 +117,8 @@ const PHONE = /^\+?[0-9]{4,17}$/;
|
|
|
90
117
|
const get = {
|
|
91
118
|
summary: 'people by user id, e-mail or phone (exact lookup)',
|
|
92
119
|
usage: 'wx user get [<id|email|phone>...] [--id USERID]... [--email EMAIL]... [--phone PHONE]...',
|
|
93
|
-
details:
|
|
120
|
+
details:
|
|
121
|
+
'A value given without an option is an e-mail when it has "@", a phone when it is digits (+ allowed), else a user id. For a search by name: wx user find.',
|
|
94
122
|
options: {
|
|
95
123
|
id: {type: 'string', multiple: true, placeholder: 'USERID', description: 'a user id'},
|
|
96
124
|
email: {type: 'string', multiple: true, placeholder: 'EMAIL', description: 'an e-mail address'},
|
|
@@ -111,7 +139,8 @@ const get = {
|
|
|
111
139
|
if (ids.length) q.ids = ids.map((id) => checkId('The user id', id));
|
|
112
140
|
if (emails.length) q.emails = emails;
|
|
113
141
|
if (phones.length) q.phones = phones;
|
|
114
|
-
if (!Object.keys(q).length)
|
|
142
|
+
if (!Object.keys(q).length)
|
|
143
|
+
throw new UsageError('Give a user id, an e-mail or a phone (or --id, --email, --phone)', ctx.usage());
|
|
115
144
|
return api(ctx).call('POST', '/users/query', {query: q});
|
|
116
145
|
},
|
|
117
146
|
text(r, ctx) {
|