@velaro/cli 1.4.66 → 1.4.68
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/lib/commands/focus.js +27 -7
- package/lib/commands/routing.js +14 -3
- package/lib/routing-inactivity.js +74 -3
- package/lib/routing-inactivity.test.js +45 -2
- package/package.json +1 -1
package/lib/commands/focus.js
CHANGED
|
@@ -220,19 +220,39 @@ const triageRulesCommand = {
|
|
|
220
220
|
};
|
|
221
221
|
|
|
222
222
|
const setTriageRuleCommand = {
|
|
223
|
-
command: 'set-triage-rule
|
|
224
|
-
describe: `Enable
|
|
223
|
+
command: 'set-triage-rule [label] [enabled]',
|
|
224
|
+
describe: `Enable/disable one fixed AI triage label (${VALID_LABELS.join(', ')}) and/or set --triage-mode`,
|
|
225
225
|
builder: (y) => y
|
|
226
226
|
.positional('label', { type: 'string', describe: `Label: ${VALID_LABELS.join(', ')}` })
|
|
227
|
-
.positional('enabled', { type: 'string', describe: 'true or false', choices: ['true', 'false'] })
|
|
227
|
+
.positional('enabled', { type: 'string', describe: 'true or false', choices: ['true', 'false'] })
|
|
228
|
+
.option('triage-mode', { type: 'string', choices: ['off', 'economy'], describe: 'off (default) or economy (skip AI call for obvious messages)' }),
|
|
228
229
|
handler: async (argv) => {
|
|
229
230
|
const { label, enabled } = argv;
|
|
230
|
-
|
|
231
|
-
|
|
231
|
+
const mode = argv['triage-mode'];
|
|
232
|
+
if (!label && !mode) {
|
|
233
|
+
console.error('Provide <label> <enabled> and/or --triage-mode <off|economy>.');
|
|
232
234
|
process.exit(1);
|
|
233
235
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
+
let labels;
|
|
237
|
+
if (label) {
|
|
238
|
+
if (!VALID_LABELS.includes(label)) {
|
|
239
|
+
console.error(`Invalid label "${label}". Valid values: ${VALID_LABELS.join(', ')}`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
if (enabled !== 'true' && enabled !== 'false') {
|
|
243
|
+
console.error('<enabled> must be true or false.');
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
labels = [{ Label: label, Enabled: enabled === 'true' }];
|
|
247
|
+
}
|
|
248
|
+
const body = mode ? { ...(labels ? { labels } : {}), triageMode: mode } : labels;
|
|
249
|
+
const result = await messagingPut('/Focus/triage-rules', body);
|
|
250
|
+
if (label) console.log(`
|
|
251
|
+
${GRN}✓${RESET} Label "${label}" ${enabled === 'true' ? 'enabled' : 'disabled'}.`);
|
|
252
|
+
if (mode) console.log(`
|
|
253
|
+
${GRN}✓${RESET} Triage mode set to ${mode}.`);
|
|
254
|
+
console.log(` Effective labels: ${(result?.effectiveLabels ?? []).join(', ')}
|
|
255
|
+
`);
|
|
236
256
|
},
|
|
237
257
|
};
|
|
238
258
|
|
package/lib/commands/routing.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { messagingGet, messagingPut, messagingPost, messagingDel, get as adminGet } from '../api.js';
|
|
2
2
|
import {
|
|
3
3
|
TERMINAL_ACTION_NAMES, buildPolicyUpdate, validatePolicyUpdate, describeChain,
|
|
4
|
-
describeSimulation, describeTransfers,
|
|
4
|
+
describeSimulation, describeTransfers, validateAdvancedUpdate,
|
|
5
5
|
} from '../routing-inactivity.js';
|
|
6
6
|
|
|
7
7
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
@@ -199,13 +199,24 @@ const policySetCommand = {
|
|
|
199
199
|
.option('queue-escalation-minutes', { type: 'number', describe: 'Minutes in queue before escalation alert (1-240)' })
|
|
200
200
|
.option('terminal-workflow', { type: 'number', describe: 'Workflow id to run when the terminal action is RunWorkflow' })
|
|
201
201
|
.option('visitor-text', { type: 'string', describe: 'Visitor status text (max 500). Tokens: {{releasedFromAgent}} {{reason}} {{teamName}}' })
|
|
202
|
-
.option('agent-text', { type: 'string', describe: 'Agent status text (max 500)' })
|
|
202
|
+
.option('agent-text', { type: 'string', describe: 'Agent status text (max 500)' })
|
|
203
|
+
.option('no-typing-after', { type: 'number', describe: 'Advanced: seconds with no agent typing before the stepped-away steps run (10-3600)' })
|
|
204
|
+
.option('no-typing-message', { type: 'string', describe: 'Advanced: message to the visitor when the agent steps away (max 500)' })
|
|
205
|
+
.option('no-typing-alert-supervisor', { type: 'boolean', describe: 'Advanced: alert a supervisor when the agent stops typing' })
|
|
206
|
+
.option('no-typing-reroute', { type: 'boolean', describe: 'Advanced: move the chat to another agent when the agent stops typing' })
|
|
207
|
+
.option('escalation-team', { type: 'number', describe: 'Advanced: group alerted when the chat stays unattended' })
|
|
208
|
+
.option('escalation-after', { type: 'number', describe: 'Advanced: seconds unattended before the escalation group is alerted (30-14400)' })
|
|
209
|
+
.option('terminal-bot', { type: 'number', describe: 'Advanced: bot (AI configuration id) for terminal action ReturnToBot (9)' })
|
|
210
|
+
.option('apply-to', { type: 'string', choices: ['auto-route', 'queue'], describe: 'Advanced: apply to auto-routing only or the queue only (omit = unchanged)' }),
|
|
203
211
|
handler: async (argv) => {
|
|
204
212
|
// Load current policy first, then merge
|
|
205
213
|
const current = await messagingGet('/AutoRoutingPolicy');
|
|
206
214
|
|
|
207
215
|
const updated = buildPolicyUpdate(current, argv);
|
|
208
|
-
const problems =
|
|
216
|
+
const problems = [
|
|
217
|
+
...validatePolicyUpdate(updated),
|
|
218
|
+
...validateAdvancedUpdate(updated.advanced, updated.terminalActionOverride, current),
|
|
219
|
+
];
|
|
209
220
|
if (problems.length) {
|
|
210
221
|
for (const m of problems) console.error(` ${RED}x${RESET} ${m}`);
|
|
211
222
|
process.exitCode = 1;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Persisted as ints by messaging (append-only enum); the API accepts and returns the NAME.
|
|
5
5
|
export const TERMINAL_ACTION_NAMES = [
|
|
6
6
|
'Survey', 'CreateTicket', 'SurveyAndCreateTicket', 'Queue', 'OfflineMessage', 'NoOp',
|
|
7
|
-
'EndChat', 'NotifyManager', 'RunWorkflow',
|
|
7
|
+
'EndChat', 'NotifyManager', 'RunWorkflow', 'ReturnToBot',
|
|
8
8
|
];
|
|
9
9
|
|
|
10
10
|
export function terminalLabel(v) {
|
|
@@ -18,7 +18,7 @@ export function terminalForSave(v) {
|
|
|
18
18
|
if (v === null || v === undefined || v === '') return null;
|
|
19
19
|
if (typeof v === 'number' || /^\d+$/.test(String(v))) {
|
|
20
20
|
const name = TERMINAL_ACTION_NAMES[Number(v)];
|
|
21
|
-
if (!name) throw new Error(`Unknown terminal action ${v}. Use 0-
|
|
21
|
+
if (!name) throw new Error(`Unknown terminal action ${v}. Use 0-9: ${TERMINAL_ACTION_NAMES.map((n, i) => `${i}=${n}`).join(' ')}`);
|
|
22
22
|
return name;
|
|
23
23
|
}
|
|
24
24
|
const hit = TERMINAL_ACTION_NAMES.find(n => n.toLowerCase() === String(v).toLowerCase());
|
|
@@ -47,6 +47,72 @@ export const CHAIN_FLAGS = {
|
|
|
47
47
|
* reassignmentMode is ALWAYS sent (an absent value is treated as Observe by the API and would silently
|
|
48
48
|
* switch reassignment off). The chain block is only sent when the server reports one or a chain flag is set.
|
|
49
49
|
*/
|
|
50
|
+
// CLI flag -> AutoRoutingPolicy.advanced field (stepped away / not typing). null on PUT = unchanged.
|
|
51
|
+
export const ADVANCED_FLAGS = {
|
|
52
|
+
'no-typing-after': 'noTypingAfterSeconds',
|
|
53
|
+
'no-typing-message': 'noTypingVisitorMessage',
|
|
54
|
+
'no-typing-alert-supervisor': 'noTypingAlertSupervisor',
|
|
55
|
+
'no-typing-reroute': 'noTypingReroute',
|
|
56
|
+
'escalation-team': 'escalationTeamId',
|
|
57
|
+
'escalation-after': 'escalationAfterSeconds',
|
|
58
|
+
'terminal-bot': 'terminalBotProfileId',
|
|
59
|
+
'apply-to': 'applyToRoutingActions',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const APPLY_TO = { acd: 1, 'auto-route': 1, queue: 2 };
|
|
63
|
+
|
|
64
|
+
/** Returns the `advanced` PUT block for the flags given (others null = unchanged), or null when no advanced flag was passed. */
|
|
65
|
+
export function buildAdvancedUpdate(current, argv) {
|
|
66
|
+
const given = Object.keys(ADVANCED_FLAGS).filter(f => argv[f] !== undefined);
|
|
67
|
+
if (!given.length) return null;
|
|
68
|
+
if (!('advanced' in current)) throw new Error('This messaging build does not report the advanced stepped-away settings yet; they cannot be saved.');
|
|
69
|
+
const out = Object.fromEntries(Object.values(ADVANCED_FLAGS).map(k => [k, null]));
|
|
70
|
+
for (const f of given) {
|
|
71
|
+
let v = argv[f];
|
|
72
|
+
if (f === 'apply-to') {
|
|
73
|
+
v = typeof v === 'number' ? v : APPLY_TO[String(v).toLowerCase()];
|
|
74
|
+
if (v !== 1 && v !== 2) throw new Error('--apply-to must be auto-route or queue. Omit it to leave the setting unchanged.');
|
|
75
|
+
}
|
|
76
|
+
out[ADVANCED_FLAGS[f]] = v;
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Cross-field/range checks for the advanced block, mirroring the server. Returns a list of messages. */
|
|
82
|
+
export function validateAdvancedUpdate(adv, terminalAction, current) {
|
|
83
|
+
const errors = [];
|
|
84
|
+
if (adv) {
|
|
85
|
+
const i = Number.isInteger;
|
|
86
|
+
if (adv.noTypingAfterSeconds != null && !(i(adv.noTypingAfterSeconds) && adv.noTypingAfterSeconds >= 10 && adv.noTypingAfterSeconds <= 3600))
|
|
87
|
+
errors.push('--no-typing-after must be a whole number from 10 to 3600.');
|
|
88
|
+
if (adv.escalationAfterSeconds != null && !(i(adv.escalationAfterSeconds) && adv.escalationAfterSeconds >= 30 && adv.escalationAfterSeconds <= 14400))
|
|
89
|
+
errors.push('--escalation-after must be a whole number from 30 to 14400.');
|
|
90
|
+
if (typeof adv.noTypingVisitorMessage === 'string' && adv.noTypingVisitorMessage.length > 500)
|
|
91
|
+
errors.push('--no-typing-message must be 500 characters or fewer.');
|
|
92
|
+
if (adv.escalationAfterSeconds != null && adv.escalationTeamId == null && current?.advanced?.escalationTeamId == null)
|
|
93
|
+
errors.push('--escalation-after needs --escalation-team <id>.');
|
|
94
|
+
}
|
|
95
|
+
if (terminalAction === 'ReturnToBot' && adv?.terminalBotProfileId == null && current?.advanced?.terminalBotProfileId == null)
|
|
96
|
+
errors.push('Terminal action ReturnToBot needs --terminal-bot <id>.');
|
|
97
|
+
return errors;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function describeAdvanced(adv) {
|
|
101
|
+
if (!adv) return [' (This messaging build does not report the advanced stepped-away settings.)'];
|
|
102
|
+
const from = f => (adv.inheritedFrom?.[f] ? ` [from ${adv.inheritedFrom[f]}]` : '');
|
|
103
|
+
const v = x => (x === null || x === undefined ? 'not set' : x);
|
|
104
|
+
return [
|
|
105
|
+
'Advanced (stepped away / not typing):',
|
|
106
|
+
` No typing after: ${adv.noTypingAfterSeconds != null ? `${adv.noTypingAfterSeconds}s` : 'off'}${from('noTypingAfterSeconds')}`,
|
|
107
|
+
` Visitor message: ${v(adv.noTypingVisitorMessage)}${from('noTypingVisitorMessage')}`,
|
|
108
|
+
` Alert supervisor: ${v(adv.noTypingAlertSupervisor)}${from('noTypingAlertSupervisor')}`,
|
|
109
|
+
` Move to next agent: ${v(adv.noTypingReroute)}${from('noTypingReroute')}`,
|
|
110
|
+
` Escalation group: ${v(adv.escalationTeamId)}${adv.escalationAfterSeconds != null ? ` after ${adv.escalationAfterSeconds}s` : ''}${from('escalationTeamId')}`,
|
|
111
|
+
` Return-to-bot bot: ${v(adv.terminalBotProfileId)}${from('terminalBotProfileId')}`,
|
|
112
|
+
` Applies to: ${adv.applyToRoutingActions === 1 ? 'auto-routing only' : adv.applyToRoutingActions === 2 ? 'queue only' : 'auto-routing and queue'}${from('applyToRoutingActions')}`,
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
|
|
50
116
|
export function buildPolicyUpdate(current, argv) {
|
|
51
117
|
const merged = { ...current };
|
|
52
118
|
const map = {
|
|
@@ -64,7 +130,7 @@ export function buildPolicyUpdate(current, argv) {
|
|
|
64
130
|
argv['terminal-action'] !== undefined ? argv['terminal-action'] : current.terminalActionOverride
|
|
65
131
|
);
|
|
66
132
|
// Actions 6-8 exist only on a messaging build with the inactivity chain; an older one would store a different value.
|
|
67
|
-
if (['EndChat', 'NotifyManager', 'RunWorkflow'].includes(merged.terminalActionOverride) && !('inactivityChain' in current))
|
|
133
|
+
if (['EndChat', 'NotifyManager', 'RunWorkflow', 'ReturnToBot'].includes(merged.terminalActionOverride) && !('inactivityChain' in current))
|
|
68
134
|
throw new Error(`Terminal action ${merged.terminalActionOverride} needs a messaging build with the agent inactivity chain; this one does not report it. Use 0-5.`);
|
|
69
135
|
|
|
70
136
|
const mode = argv['reassignment-mode'] ?? current.reassignmentMode ?? 'Observe';
|
|
@@ -79,6 +145,10 @@ export function buildPolicyUpdate(current, argv) {
|
|
|
79
145
|
for (const f of patched) chain[CHAIN_FLAGS[f]] = argv[f];
|
|
80
146
|
merged.inactivityChain = chain;
|
|
81
147
|
}
|
|
148
|
+
// The read-side advanced block carries inheritedFrom; send only the explicit PUT block (or nothing).
|
|
149
|
+
delete merged.advanced;
|
|
150
|
+
const adv = buildAdvancedUpdate(current, argv);
|
|
151
|
+
if (adv) merged.advanced = adv;
|
|
82
152
|
return merged;
|
|
83
153
|
}
|
|
84
154
|
|
|
@@ -117,6 +187,7 @@ export function describeChain(p) {
|
|
|
117
187
|
` Terminal workflow: ${c.terminalWorkflowId ?? 'none'}`,
|
|
118
188
|
);
|
|
119
189
|
}
|
|
190
|
+
if ('advanced' in p) out.push(...describeAdvanced(p.advanced));
|
|
120
191
|
return out;
|
|
121
192
|
}
|
|
122
193
|
|
|
@@ -2,7 +2,7 @@ import { test } from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import {
|
|
4
4
|
buildPolicyUpdate, validatePolicyUpdate, terminalForSave, terminalLabel,
|
|
5
|
-
describeChain, describeSimulation, describeTransfers,
|
|
5
|
+
describeChain, describeSimulation, describeTransfers, buildAdvancedUpdate, validateAdvancedUpdate, describeAdvanced,
|
|
6
6
|
} from './routing-inactivity.js';
|
|
7
7
|
|
|
8
8
|
const current = {
|
|
@@ -15,7 +15,7 @@ test('terminal action accepts ints and names, rejects junk', () => {
|
|
|
15
15
|
assert.equal(terminalForSave('7'), 'NotifyManager');
|
|
16
16
|
assert.equal(terminalForSave('runworkflow'), 'RunWorkflow');
|
|
17
17
|
assert.equal(terminalForSave(undefined), null);
|
|
18
|
-
assert.throws(() => terminalForSave(
|
|
18
|
+
assert.throws(() => terminalForSave(10), /Unknown terminal action/);
|
|
19
19
|
assert.throws(() => terminalForSave('Bogus'), /Unknown terminal action/);
|
|
20
20
|
assert.equal(terminalLabel(null), 'Default (Survey)');
|
|
21
21
|
assert.equal(terminalLabel(8), 'RunWorkflow');
|
|
@@ -99,3 +99,46 @@ test('describe helpers render the chain, a simulation and a transfer count', ()
|
|
|
99
99
|
assert.match(describeTransfers({ siteId: 1, days: 30, total: 2, byAgent: [{ agentId: 7, name: 'Sam', count: 2 }], byDay: [] }).join('\n'), /Sam \(id 7\): 2/);
|
|
100
100
|
assert.deepEqual(describeTransfers({ note: 'No agent found' }), ['No agent found']);
|
|
101
101
|
});
|
|
102
|
+
|
|
103
|
+
const withAdv = { ...current, advanced: { noTypingAfterSeconds: null, inheritedFrom: { noTypingAfterSeconds: 'Default' } } };
|
|
104
|
+
|
|
105
|
+
test('ReturnToBot is terminal action 9', () => {
|
|
106
|
+
assert.equal(terminalForSave(9), 'ReturnToBot');
|
|
107
|
+
assert.equal(terminalForSave('returntobot'), 'ReturnToBot');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('advanced flags build a PUT block where untouched fields are null (unchanged)', () => {
|
|
111
|
+
const body = buildPolicyUpdate(withAdv, { 'no-typing-after': 90, 'no-typing-reroute': true, 'apply-to': 'queue' });
|
|
112
|
+
assert.equal(body.advanced.noTypingAfterSeconds, 90);
|
|
113
|
+
assert.equal(body.advanced.noTypingReroute, true);
|
|
114
|
+
assert.equal(body.advanced.applyToRoutingActions, 2);
|
|
115
|
+
assert.equal(body.advanced.escalationTeamId, null);
|
|
116
|
+
assert.equal(body.advanced.inheritedFrom, undefined);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('no advanced flag means the read-side advanced block is not echoed back', () => {
|
|
120
|
+
const body = buildPolicyUpdate(withAdv, { 'max-reroutes': 4 });
|
|
121
|
+
assert.equal('advanced' in body, false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('advanced flags fail clearly on a messaging build without the block', () => {
|
|
125
|
+
assert.throws(() => buildPolicyUpdate(current, { 'no-typing-after': 90 }), /does not report the advanced/);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('advanced validation mirrors the server rules', () => {
|
|
129
|
+
const adv = buildAdvancedUpdate(withAdv, { 'no-typing-after': 5, 'escalation-after': 10 });
|
|
130
|
+
const errs = validateAdvancedUpdate(adv, null, withAdv).join(' | ');
|
|
131
|
+
assert.match(errs, /--no-typing-after/);
|
|
132
|
+
assert.match(errs, /--escalation-after must be/);
|
|
133
|
+
assert.match(errs, /needs --escalation-team/);
|
|
134
|
+
assert.match(validateAdvancedUpdate(null, 'ReturnToBot', withAdv).join(' '), /needs --terminal-bot/);
|
|
135
|
+
assert.deepEqual(validateAdvancedUpdate(buildAdvancedUpdate(withAdv, { 'terminal-bot': 4 }), 'ReturnToBot', withAdv), []);
|
|
136
|
+
assert.throws(() => buildAdvancedUpdate(withAdv, { 'apply-to': 'both' }), /--apply-to/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('describeAdvanced shows the value and where it is inherited from', () => {
|
|
140
|
+
const text = describeAdvanced({ noTypingAfterSeconds: 90, inheritedFrom: { noTypingAfterSeconds: 'Team' } }).join('\n');
|
|
141
|
+
assert.match(text, /No typing after:\s+90s \[from Team\]/);
|
|
142
|
+
assert.match(describeAdvanced(undefined).join(' '), /does not report/);
|
|
143
|
+
assert.match(describeChain(withAdv).join('\n'), /Advanced \(stepped away/);
|
|
144
|
+
});
|
package/package.json
CHANGED