@tiledesk/tiledesk-tybot-connector 2.1.2 → 2.1.5
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/AnalyticsClient.js +21 -3
- package/CHANGELOG.md +3 -0
- package/TiledeskWhenExpression.js +541 -0
- package/engine/TiledeskChatbot.js +12 -9
- package/index.js +11 -9
- package/logs/app1.log +2971 -0
- package/package.json +1 -1
- package/services/DataTablesService.js +180 -0
- package/tiledeskChatbotPlugs/DirectivesChatbotPlug.js +38 -19
- package/tiledeskChatbotPlugs/directives/DirAiCondition.js +1 -1
- package/tiledeskChatbotPlugs/directives/DirAiPrompt.js +1 -1
- package/tiledeskChatbotPlugs/directives/DirAskGPT.js +1 -1
- package/tiledeskChatbotPlugs/directives/DirAskGPTV2.js +1 -1
- package/tiledeskChatbotPlugs/directives/DirDataTables.js +293 -0
- package/tiledeskChatbotPlugs/directives/DirJSONConditionV2.js +154 -0
- package/tiledeskChatbotPlugs/directives/DirMoveToAgent.js +13 -10
- package/tiledeskChatbotPlugs/directives/DirReplaceBotV2.js +10 -7
- package/tiledeskChatbotPlugs/directives/DirReplaceBotV3.js +10 -7
- package/tiledeskChatbotPlugs/directives/Directives.js +2 -1
package/AnalyticsClient.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const axios = require('axios');
|
|
4
4
|
const { v4: uuidv4 } = require('uuid');
|
|
5
|
+
const winston = require('./utils/winston');
|
|
5
6
|
|
|
6
7
|
class AnalyticsClient {
|
|
7
8
|
|
|
@@ -17,7 +18,10 @@ class AnalyticsClient {
|
|
|
17
18
|
static track(eventType, projectId, payload) {
|
|
18
19
|
// Read env vars at call time so tests (and runtime reconfig) can override them.
|
|
19
20
|
const ingestUrl = process.env.ANALYTICS_INGEST_URL;
|
|
20
|
-
if (!ingestUrl)
|
|
21
|
+
if (!ingestUrl) {
|
|
22
|
+
winston.debug("(AnalyticsClient) ANALYTICS_INGEST_URL not set; skipping event_type=" + eventType);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
21
25
|
|
|
22
26
|
const event = {
|
|
23
27
|
event_id: uuidv4(),
|
|
@@ -35,12 +39,26 @@ class AnalyticsClient {
|
|
|
35
39
|
headers['X-Api-Key'] = apiKey;
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
// [analytics-debug] Log the POST and its outcome so dropped/rejected events
|
|
43
|
+
// (e.g. ingest 422 validation failures, 503 broker-down) are visible in the
|
|
44
|
+
// chatbot logs instead of being silently swallowed.
|
|
45
|
+
winston.debug("(AnalyticsClient) POST /events event_type=" + eventType + " event_id=" + event.event_id, payload);
|
|
46
|
+
|
|
38
47
|
axios.post(
|
|
39
48
|
ingestUrl + '/events',
|
|
40
49
|
event,
|
|
41
50
|
{ headers, timeout: 3000 }
|
|
42
|
-
).
|
|
43
|
-
|
|
51
|
+
).then((res) => {
|
|
52
|
+
winston.debug("(AnalyticsClient) event accepted event_type=" + eventType + " event_id=" + event.event_id + " status=" + res.status);
|
|
53
|
+
}).catch((err) => {
|
|
54
|
+
// fire-and-forget: never propagate to chatbot, but log so drops are visible.
|
|
55
|
+
const status = err.response ? err.response.status : undefined;
|
|
56
|
+
const body = err.response ? err.response.data : undefined;
|
|
57
|
+
winston.warn("(AnalyticsClient) event REJECTED/FAILED event_type=" + eventType +
|
|
58
|
+
" event_id=" + event.event_id +
|
|
59
|
+
" status=" + status +
|
|
60
|
+
" error=" + (err.message || err) +
|
|
61
|
+
" response=" + (body ? JSON.stringify(body) : '<none>'));
|
|
44
62
|
});
|
|
45
63
|
}
|
|
46
64
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
available on:
|
|
6
6
|
▶️ https://www.npmjs.com/package/@tiledesk/tiledesk-tybot-connector
|
|
7
7
|
|
|
8
|
+
# this branch
|
|
9
|
+
- added: JSON Condition V2 runtime — new `jsoncondition2` type routed to `DirJSONConditionV2` (evaluates the `when` expression via `TiledeskWhenExpression`, no eval/vm2); V1 `jsoncondition` dispatch left unchanged (`DirJSONCondition`) for full backward-compatibility
|
|
10
|
+
|
|
8
11
|
# 2.1.0
|
|
9
12
|
- added: support to analytics
|
|
10
13
|
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TiledeskWhenExpression
|
|
3
|
+
*
|
|
4
|
+
* Safe evaluator for the new `when` field of JSON conditions.
|
|
5
|
+
*
|
|
6
|
+
* Design goals (see security review of TiledeskExpression.evaluateJavascriptExpression):
|
|
7
|
+
* - NO `eval`, NO `Function`, NO `vm`/`vm2`. The expression is parsed into an AST
|
|
8
|
+
* and interpreted by walking the tree. Code is never generated from data.
|
|
9
|
+
* - Whitelisted operators and whitelisted functions only.
|
|
10
|
+
* - Property access is blocked on dangerous keys (__proto__, prototype, constructor).
|
|
11
|
+
* - Function calls are allowed ONLY for plain identifiers present in the function
|
|
12
|
+
* whitelist (no method calls on arbitrary objects -> blocks `x.constructor(...)`).
|
|
13
|
+
* - Identifiers resolve exclusively against the provided variables map (own props only),
|
|
14
|
+
* so there is no access to host globals.
|
|
15
|
+
* - Hard limits on input length and recursion depth.
|
|
16
|
+
*
|
|
17
|
+
* This file lives ALONGSIDE the legacy TiledeskExpression.js, which is left untouched.
|
|
18
|
+
*
|
|
19
|
+
* Public API:
|
|
20
|
+
* const { TiledeskWhenExpression } = require('./TiledeskWhenExpression');
|
|
21
|
+
* const value = new TiledeskWhenExpression().evaluate(whenString, variables);
|
|
22
|
+
* // throws WhenSyntaxError / WhenEvalError on failure (callers decide how to route)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
class WhenSyntaxError extends Error {
|
|
26
|
+
constructor(message) { super(message); this.name = 'WhenSyntaxError'; }
|
|
27
|
+
}
|
|
28
|
+
class WhenEvalError extends Error {
|
|
29
|
+
constructor(message) { super(message); this.name = 'WhenEvalError'; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const TOKEN = { NUM: 'num', STR: 'str', IDENT: 'ident', OP: 'op', PUNC: 'punc', EOF: 'eof' };
|
|
33
|
+
|
|
34
|
+
// Binary operator precedence (higher binds tighter). Unary operators handled separately.
|
|
35
|
+
const BIN_PRECEDENCE = {
|
|
36
|
+
'||': 1,
|
|
37
|
+
'&&': 2,
|
|
38
|
+
'==': 3, '!=': 3, '===': 3, '!==': 3,
|
|
39
|
+
'<': 4, '<=': 4, '>': 4, '>=': 4,
|
|
40
|
+
'+': 5, '-': 5,
|
|
41
|
+
'*': 6, '/': 6, '%': 6,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const FORBIDDEN_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
|
|
45
|
+
|
|
46
|
+
const MAX_EXPRESSION_LENGTH = 10000;
|
|
47
|
+
const MAX_DEPTH = 100;
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Tokenizer
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
function isIdentStart(c) { return /[A-Za-z_$]/.test(c); }
|
|
54
|
+
function isIdentPart(c) { return /[A-Za-z0-9_$]/.test(c); }
|
|
55
|
+
function isDigit(c) { return c >= '0' && c <= '9'; }
|
|
56
|
+
|
|
57
|
+
function tokenize(src) {
|
|
58
|
+
const tokens = [];
|
|
59
|
+
let i = 0;
|
|
60
|
+
const n = src.length;
|
|
61
|
+
|
|
62
|
+
while (i < n) {
|
|
63
|
+
const c = src[i];
|
|
64
|
+
|
|
65
|
+
// whitespace
|
|
66
|
+
if (c === ' ' || c === '\t' || c === '\n' || c === '\r') { i++; continue; }
|
|
67
|
+
|
|
68
|
+
// string literal
|
|
69
|
+
if (c === '"' || c === "'") {
|
|
70
|
+
const quote = c;
|
|
71
|
+
i++;
|
|
72
|
+
let s = '';
|
|
73
|
+
while (i < n && src[i] !== quote) {
|
|
74
|
+
if (src[i] === '\\') {
|
|
75
|
+
i++;
|
|
76
|
+
const e = src[i];
|
|
77
|
+
if (e === 'n') s += '\n';
|
|
78
|
+
else if (e === 't') s += '\t';
|
|
79
|
+
else if (e === 'r') s += '\r';
|
|
80
|
+
else if (e === '\\') s += '\\';
|
|
81
|
+
else if (e === '"') s += '"';
|
|
82
|
+
else if (e === "'") s += "'";
|
|
83
|
+
else s += (e === undefined ? '' : e);
|
|
84
|
+
i++;
|
|
85
|
+
} else {
|
|
86
|
+
s += src[i];
|
|
87
|
+
i++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (i >= n) throw new WhenSyntaxError('Unterminated string literal');
|
|
91
|
+
i++; // consume closing quote
|
|
92
|
+
tokens.push({ type: TOKEN.STR, value: s });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// number literal
|
|
97
|
+
if (isDigit(c) || (c === '.' && isDigit(src[i + 1]))) {
|
|
98
|
+
const start = i;
|
|
99
|
+
while (i < n && isDigit(src[i])) i++;
|
|
100
|
+
if (src[i] === '.') { i++; while (i < n && isDigit(src[i])) i++; }
|
|
101
|
+
if (src[i] === 'e' || src[i] === 'E') {
|
|
102
|
+
i++;
|
|
103
|
+
if (src[i] === '+' || src[i] === '-') i++;
|
|
104
|
+
while (i < n && isDigit(src[i])) i++;
|
|
105
|
+
}
|
|
106
|
+
tokens.push({ type: TOKEN.NUM, value: Number(src.slice(start, i)) });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// identifier / keyword
|
|
111
|
+
if (isIdentStart(c)) {
|
|
112
|
+
const start = i;
|
|
113
|
+
i++;
|
|
114
|
+
while (i < n && isIdentPart(src[i])) i++;
|
|
115
|
+
tokens.push({ type: TOKEN.IDENT, value: src.slice(start, i) });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// multi-char operators (longest match first)
|
|
120
|
+
const three = src.slice(i, i + 3);
|
|
121
|
+
if (three === '===' || three === '!==') { tokens.push({ type: TOKEN.OP, value: three }); i += 3; continue; }
|
|
122
|
+
|
|
123
|
+
const two = src.slice(i, i + 2);
|
|
124
|
+
if (two === '?.') { tokens.push({ type: TOKEN.PUNC, value: '?.' }); i += 2; continue; }
|
|
125
|
+
if (two === '==' || two === '!=' || two === '<=' || two === '>=' || two === '&&' || two === '||') {
|
|
126
|
+
tokens.push({ type: TOKEN.OP, value: two }); i += 2; continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// single-char operators
|
|
130
|
+
if ('+-*/%<>!'.includes(c)) { tokens.push({ type: TOKEN.OP, value: c }); i++; continue; }
|
|
131
|
+
|
|
132
|
+
// punctuators
|
|
133
|
+
if ('()[],.'.includes(c)) { tokens.push({ type: TOKEN.PUNC, value: c }); i++; continue; }
|
|
134
|
+
|
|
135
|
+
throw new WhenSyntaxError("Unexpected character '" + c + "' at position " + i);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
tokens.push({ type: TOKEN.EOF, value: null });
|
|
139
|
+
return tokens;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Parser (recursive descent + precedence climbing)
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
class Parser {
|
|
147
|
+
constructor(tokens) { this.tokens = tokens; this.pos = 0; }
|
|
148
|
+
|
|
149
|
+
peek() { return this.tokens[this.pos]; }
|
|
150
|
+
next() { return this.tokens[this.pos++]; }
|
|
151
|
+
|
|
152
|
+
expectPunc(value) {
|
|
153
|
+
const t = this.next();
|
|
154
|
+
if (!(t.type === TOKEN.PUNC && t.value === value)) {
|
|
155
|
+
throw new WhenSyntaxError("Expected '" + value + "' but found '" + t.value + "'");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
parse() {
|
|
160
|
+
const node = this.parseExpression();
|
|
161
|
+
if (this.peek().type !== TOKEN.EOF) {
|
|
162
|
+
throw new WhenSyntaxError("Unexpected token '" + this.peek().value + "'");
|
|
163
|
+
}
|
|
164
|
+
return node;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
parseExpression() { return this.parseBinary(0); }
|
|
168
|
+
|
|
169
|
+
parseBinary(minPrec) {
|
|
170
|
+
let left = this.parseUnary();
|
|
171
|
+
while (true) {
|
|
172
|
+
const t = this.peek();
|
|
173
|
+
if (t.type !== TOKEN.OP) break;
|
|
174
|
+
const prec = BIN_PRECEDENCE[t.value];
|
|
175
|
+
if (prec === undefined || prec < minPrec) break;
|
|
176
|
+
this.next();
|
|
177
|
+
const right = this.parseBinary(prec + 1); // left-associative
|
|
178
|
+
if (t.value === '&&' || t.value === '||') {
|
|
179
|
+
left = { type: 'Logical', op: t.value, left, right };
|
|
180
|
+
} else {
|
|
181
|
+
left = { type: 'Binary', op: t.value, left, right };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return left;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
parseUnary() {
|
|
188
|
+
const t = this.peek();
|
|
189
|
+
if (t.type === TOKEN.OP && (t.value === '!' || t.value === '-' || t.value === '+')) {
|
|
190
|
+
this.next();
|
|
191
|
+
return { type: 'Unary', op: t.value, arg: this.parseUnary() };
|
|
192
|
+
}
|
|
193
|
+
return this.parsePostfix();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
parsePostfix() {
|
|
197
|
+
let node = this.parsePrimary();
|
|
198
|
+
while (true) {
|
|
199
|
+
const t = this.peek();
|
|
200
|
+
if (t.type === TOKEN.PUNC && t.value === '.') {
|
|
201
|
+
this.next();
|
|
202
|
+
const id = this.next();
|
|
203
|
+
if (id.type !== TOKEN.IDENT) throw new WhenSyntaxError("Expected property name after '.'");
|
|
204
|
+
node = { type: 'Member', object: node, property: id.value, computed: false, optional: false };
|
|
205
|
+
} else if (t.type === TOKEN.PUNC && t.value === '?.') {
|
|
206
|
+
this.next();
|
|
207
|
+
if (this.peek().type === TOKEN.PUNC && this.peek().value === '[') {
|
|
208
|
+
this.next();
|
|
209
|
+
const expr = this.parseExpression();
|
|
210
|
+
this.expectPunc(']');
|
|
211
|
+
node = { type: 'Member', object: node, property: expr, computed: true, optional: true };
|
|
212
|
+
} else {
|
|
213
|
+
const id = this.next();
|
|
214
|
+
if (id.type !== TOKEN.IDENT) throw new WhenSyntaxError("Expected property name after '?.'");
|
|
215
|
+
node = { type: 'Member', object: node, property: id.value, computed: false, optional: true };
|
|
216
|
+
}
|
|
217
|
+
} else if (t.type === TOKEN.PUNC && t.value === '[') {
|
|
218
|
+
this.next();
|
|
219
|
+
const expr = this.parseExpression();
|
|
220
|
+
this.expectPunc(']');
|
|
221
|
+
node = { type: 'Member', object: node, property: expr, computed: true, optional: false };
|
|
222
|
+
} else if (t.type === TOKEN.PUNC && t.value === '(') {
|
|
223
|
+
this.next();
|
|
224
|
+
const args = [];
|
|
225
|
+
if (!(this.peek().type === TOKEN.PUNC && this.peek().value === ')')) {
|
|
226
|
+
args.push(this.parseExpression());
|
|
227
|
+
while (this.peek().type === TOKEN.PUNC && this.peek().value === ',') {
|
|
228
|
+
this.next();
|
|
229
|
+
args.push(this.parseExpression());
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
this.expectPunc(')');
|
|
233
|
+
node = { type: 'Call', callee: node, args };
|
|
234
|
+
} else {
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return node;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
parsePrimary() {
|
|
242
|
+
const t = this.next();
|
|
243
|
+
if (t.type === TOKEN.NUM) return { type: 'Literal', value: t.value };
|
|
244
|
+
if (t.type === TOKEN.STR) return { type: 'Literal', value: t.value };
|
|
245
|
+
if (t.type === TOKEN.IDENT) {
|
|
246
|
+
if (t.value === 'true') return { type: 'Literal', value: true };
|
|
247
|
+
if (t.value === 'false') return { type: 'Literal', value: false };
|
|
248
|
+
if (t.value === 'null') return { type: 'Literal', value: null };
|
|
249
|
+
if (t.value === 'undefined') return { type: 'Literal', value: undefined };
|
|
250
|
+
return { type: 'Identifier', name: t.value };
|
|
251
|
+
}
|
|
252
|
+
if (t.type === TOKEN.PUNC && t.value === '(') {
|
|
253
|
+
const node = this.parseExpression();
|
|
254
|
+
this.expectPunc(')');
|
|
255
|
+
return node;
|
|
256
|
+
}
|
|
257
|
+
throw new WhenSyntaxError("Unexpected token '" + (t.value === null ? 'EOF' : t.value) + "'");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// Evaluator helpers (type coercion mirrors the intent of the legacy operators)
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
function isNumeric(v) {
|
|
266
|
+
if (typeof v === 'number') return Number.isFinite(v);
|
|
267
|
+
if (typeof v === 'string' && v.trim() !== '') return Number.isFinite(Number(v));
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Boolean coercion per the `when` grammar (§3): only true / "true" coerce to
|
|
272
|
+
// true, only false / "false" to false; anything else coerces to false.
|
|
273
|
+
function coerceBoolean(v) {
|
|
274
|
+
if (v === true) return true;
|
|
275
|
+
if (v === false) return false;
|
|
276
|
+
if (typeof v === 'string') {
|
|
277
|
+
const s = v.trim().toLowerCase();
|
|
278
|
+
if (s === 'true') return true;
|
|
279
|
+
if (s === 'false') return false;
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Parse a value into a Date (ISO-8601 strings, epoch numbers, Date instances).
|
|
285
|
+
// Returns null when the value is missing or not a valid date.
|
|
286
|
+
function parseDate(v) {
|
|
287
|
+
if (v === null || v === undefined || v === '') return null;
|
|
288
|
+
const d = (v instanceof Date) ? v : new Date(v);
|
|
289
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Structural (deep) equality for arrays / plain objects coming from JSON
|
|
293
|
+
// variables. Used when both sides of == / != resolve to objects.
|
|
294
|
+
function deepEqual(a, b, depth) {
|
|
295
|
+
if (depth > MAX_DEPTH) return false;
|
|
296
|
+
if (a === b) return true;
|
|
297
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;
|
|
298
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
299
|
+
const ak = Object.keys(a), bk = Object.keys(b);
|
|
300
|
+
if (ak.length !== bk.length) return false;
|
|
301
|
+
for (const k of ak) {
|
|
302
|
+
if (FORBIDDEN_KEYS.has(k)) continue;
|
|
303
|
+
if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
|
|
304
|
+
if (!deepEqual(a[k], b[k], depth + 1)) return false;
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Equality whose semantics follow the RIGHT operand's type, mirroring the
|
|
310
|
+
// grammar's disambiguation of the single `==` token:
|
|
311
|
+
// x == "y" -> text (case-sensitive String comparison)
|
|
312
|
+
// x == N -> number (numeric coercion; non-numeric left -> not equal)
|
|
313
|
+
// x == true -> boolean (coerceBoolean on the left)
|
|
314
|
+
// x == otherVar -> compare by the resolved value (deep equality for objects)
|
|
315
|
+
function looseEq(l, r) {
|
|
316
|
+
if (typeof r === 'boolean') return coerceBoolean(l) === r;
|
|
317
|
+
if (typeof r === 'number') {
|
|
318
|
+
// Only real numbers / numeric strings compare numerically; arrays, objects,
|
|
319
|
+
// booleans and empty/non-numeric strings are NOT a number -> not equal.
|
|
320
|
+
return isNumeric(l) && Number(l) === r;
|
|
321
|
+
}
|
|
322
|
+
if (r !== null && typeof r === 'object') return deepEqual(l, r, 0);
|
|
323
|
+
// string / null / undefined right operand -> case-sensitive text comparison
|
|
324
|
+
return String(l) === String(r);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Relational operators (<, <=, >, >=) are numeric-only per the grammar (§3).
|
|
328
|
+
// Anything that is not a number / numeric string (arrays, objects, booleans,
|
|
329
|
+
// empty strings, missing values) yields false instead of a coerced comparison.
|
|
330
|
+
function numericCompare(op, l, r) {
|
|
331
|
+
if (!isNumeric(l) || !isNumeric(r)) return false;
|
|
332
|
+
const a = Number(l), b = Number(r);
|
|
333
|
+
switch (op) {
|
|
334
|
+
case '<': return a < b;
|
|
335
|
+
case '<=': return a <= b;
|
|
336
|
+
case '>': return a > b;
|
|
337
|
+
case '>=': return a >= b;
|
|
338
|
+
default: return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function applyBinary(op, l, r) {
|
|
343
|
+
switch (op) {
|
|
344
|
+
case '+':
|
|
345
|
+
if (isNumeric(l) && isNumeric(r)) return Number(l) + Number(r);
|
|
346
|
+
return String(l) + String(r);
|
|
347
|
+
case '-': return Number(l) - Number(r);
|
|
348
|
+
case '*': return Number(l) * Number(r);
|
|
349
|
+
case '/': return Number(l) / Number(r);
|
|
350
|
+
case '%': return Number(l) % Number(r);
|
|
351
|
+
case '==': return looseEq(l, r);
|
|
352
|
+
case '!=': return !looseEq(l, r);
|
|
353
|
+
case '===': return l === r;
|
|
354
|
+
case '!==': return l !== r;
|
|
355
|
+
case '<':
|
|
356
|
+
case '<=':
|
|
357
|
+
case '>':
|
|
358
|
+
case '>=': return numericCompare(op, l, r);
|
|
359
|
+
default: throw new WhenEvalError('Unknown binary operator: ' + op);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Whitelisted functions callable from a `when` expression. This table covers
|
|
364
|
+
// the full JSON-condition grammar (see docs/json-condition-when-grammar.md) and
|
|
365
|
+
// is the ONLY way an expression can invoke code. No function ever throws: on bad
|
|
366
|
+
// input it returns false (predicates) or a neutral value, so a condition that
|
|
367
|
+
// cannot be evaluated simply resolves falsy.
|
|
368
|
+
const DEFAULT_FUNCTIONS = {
|
|
369
|
+
// --- Text (case-sensitive; grammar §3) ---------------------------------
|
|
370
|
+
startsWith: (a, b) => String(a).startsWith(String(b)),
|
|
371
|
+
notStartsWith: (a, b) => !String(a).startsWith(String(b)),
|
|
372
|
+
endsWith: (a, b) => String(a).endsWith(String(b)),
|
|
373
|
+
contains: (a, b) => String(a).includes(String(b)),
|
|
374
|
+
matches: (a, b) => { try { return new RegExp(String(b)).test(String(a)); } catch (e) { return false; } },
|
|
375
|
+
// Legacy *IgnoreCase operators were removed by the grammar (§5); if they
|
|
376
|
+
// surface in old data, evaluate them as their case-sensitive equivalent.
|
|
377
|
+
startsWithIgnoreCase: (a, b) => String(a).startsWith(String(b)),
|
|
378
|
+
containsIgnoreCase: (a, b) => String(a).includes(String(b)),
|
|
379
|
+
|
|
380
|
+
// --- Existence / emptiness ---------------------------------------------
|
|
381
|
+
// isEmpty: null/undefined, "", [], or {} are empty; scalars are never empty.
|
|
382
|
+
isEmpty: (a) => {
|
|
383
|
+
if (a === null || a === undefined) return true;
|
|
384
|
+
if (typeof a === 'string') return a === '';
|
|
385
|
+
if (Array.isArray(a)) return a.length === 0;
|
|
386
|
+
if (typeof a === 'object') return Object.keys(a).length === 0;
|
|
387
|
+
return false;
|
|
388
|
+
},
|
|
389
|
+
isNull: (a) => a === null,
|
|
390
|
+
isUndefined: (a) => a === undefined,
|
|
391
|
+
|
|
392
|
+
// --- Date / time (ISO-8601; any invalid/missing date -> false) ---------
|
|
393
|
+
dateEqual: (a, b) => { const x = parseDate(a), y = parseDate(b); return !!(x && y) && x.getTime() === y.getTime(); },
|
|
394
|
+
isAfter: (a, b) => { const x = parseDate(a), y = parseDate(b); return !!(x && y) && x.getTime() > y.getTime(); },
|
|
395
|
+
isBefore: (a, b) => { const x = parseDate(a), y = parseDate(b); return !!(x && y) && x.getTime() < y.getTime(); },
|
|
396
|
+
isAfterOrEqual: (a, b) => { const x = parseDate(a), y = parseDate(b); return !!(x && y) && x.getTime() >= y.getTime(); },
|
|
397
|
+
isBeforeOrEqual: (a, b) => { const x = parseDate(a), y = parseDate(b); return !!(x && y) && x.getTime() <= y.getTime(); },
|
|
398
|
+
|
|
399
|
+
// --- Array -------------------------------------------------------------
|
|
400
|
+
// arrayContains: coerce the left side to an array (parse JSON strings), then
|
|
401
|
+
// membership-test comparing elements as strings (no substring false positive).
|
|
402
|
+
arrayContains: (a, b) => {
|
|
403
|
+
let arr = a;
|
|
404
|
+
if (typeof a === 'string') {
|
|
405
|
+
try { const parsed = JSON.parse(a); if (Array.isArray(parsed)) arr = parsed; } catch (e) { /* not a JSON array */ }
|
|
406
|
+
}
|
|
407
|
+
if (!Array.isArray(arr)) return false;
|
|
408
|
+
return arr.some((el) => String(el) === String(b));
|
|
409
|
+
},
|
|
410
|
+
// length: array element count, or string character length, otherwise 0.
|
|
411
|
+
length: (a) => {
|
|
412
|
+
if (Array.isArray(a)) return a.length;
|
|
413
|
+
if (typeof a === 'string') return a.length;
|
|
414
|
+
return 0;
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
// --- Scalar transforms (not condition operators, kept for expression use)
|
|
418
|
+
upperCase: (a) => String(a).toUpperCase(),
|
|
419
|
+
lowerCase: (a) => String(a).toLowerCase(),
|
|
420
|
+
capitalize: (a) => { const s = String(a); return s.charAt(0).toUpperCase() + s.slice(1); },
|
|
421
|
+
abs: (a) => Math.abs(Number(a)),
|
|
422
|
+
ceil: (a) => Math.ceil(Number(a)),
|
|
423
|
+
floor: (a) => Math.floor(Number(a)),
|
|
424
|
+
round: (a) => Math.round(Number(a)),
|
|
425
|
+
cos: (a) => Math.cos(Number(a)),
|
|
426
|
+
sin: (a) => Math.sin(Number(a)),
|
|
427
|
+
tan: (a) => Math.tan(Number(a)),
|
|
428
|
+
convertToNumber: (a) => Number(a),
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
// Evaluator
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
function evalNode(node, scope, fns, depth) {
|
|
436
|
+
if (depth > MAX_DEPTH) throw new WhenEvalError('Expression nesting too deep');
|
|
437
|
+
|
|
438
|
+
switch (node.type) {
|
|
439
|
+
case 'Literal':
|
|
440
|
+
return node.value;
|
|
441
|
+
|
|
442
|
+
case 'Identifier': {
|
|
443
|
+
const name = node.name;
|
|
444
|
+
if (FORBIDDEN_KEYS.has(name)) return undefined;
|
|
445
|
+
return Object.prototype.hasOwnProperty.call(scope, name) ? scope[name] : undefined;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
case 'Unary': {
|
|
449
|
+
const v = evalNode(node.arg, scope, fns, depth + 1);
|
|
450
|
+
if (node.op === '!') return !v;
|
|
451
|
+
if (node.op === '-') return -Number(v);
|
|
452
|
+
if (node.op === '+') return +Number(v);
|
|
453
|
+
throw new WhenEvalError('Unknown unary operator: ' + node.op);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
case 'Logical': {
|
|
457
|
+
const l = evalNode(node.left, scope, fns, depth + 1);
|
|
458
|
+
if (node.op === '&&') return l ? evalNode(node.right, scope, fns, depth + 1) : l;
|
|
459
|
+
if (node.op === '||') return l ? l : evalNode(node.right, scope, fns, depth + 1);
|
|
460
|
+
throw new WhenEvalError('Unknown logical operator: ' + node.op);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
case 'Binary': {
|
|
464
|
+
const l = evalNode(node.left, scope, fns, depth + 1);
|
|
465
|
+
const r = evalNode(node.right, scope, fns, depth + 1);
|
|
466
|
+
return applyBinary(node.op, l, r);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
case 'Member': {
|
|
470
|
+
const obj = evalNode(node.object, scope, fns, depth + 1);
|
|
471
|
+
if (obj === null || obj === undefined) return undefined; // safe + handles optional chaining
|
|
472
|
+
let key = node.computed ? evalNode(node.property, scope, fns, depth + 1) : node.property;
|
|
473
|
+
key = String(key);
|
|
474
|
+
if (FORBIDDEN_KEYS.has(key)) return undefined;
|
|
475
|
+
return obj[key];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
case 'Call': {
|
|
479
|
+
// Only plain whitelisted identifiers may be called. This blocks any method
|
|
480
|
+
// call on arbitrary objects (e.g. x.constructor(...), [].map(...)).
|
|
481
|
+
if (node.callee.type !== 'Identifier') {
|
|
482
|
+
throw new WhenEvalError('Only whitelisted function calls are allowed');
|
|
483
|
+
}
|
|
484
|
+
const fname = node.callee.name;
|
|
485
|
+
const fn = fns[fname];
|
|
486
|
+
if (typeof fn !== 'function') throw new WhenEvalError('Function not allowed: ' + fname);
|
|
487
|
+
const args = node.args.map((a) => evalNode(a, scope, fns, depth + 1));
|
|
488
|
+
return fn(...args);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
default:
|
|
492
|
+
throw new WhenEvalError('Unknown node type: ' + node.type);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ---------------------------------------------------------------------------
|
|
497
|
+
// Public class
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
|
|
500
|
+
class TiledeskWhenExpression {
|
|
501
|
+
/**
|
|
502
|
+
* @param {object} [options]
|
|
503
|
+
* @param {object} [options.functions] extra/override whitelisted functions
|
|
504
|
+
* @param {number} [options.maxLength] max expression length
|
|
505
|
+
*/
|
|
506
|
+
constructor(options) {
|
|
507
|
+
this.functions = Object.assign({}, DEFAULT_FUNCTIONS, options && options.functions);
|
|
508
|
+
this.maxLength = (options && options.maxLength) || MAX_EXPRESSION_LENGTH;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Parse only (useful for validation/tests). Throws WhenSyntaxError on failure. */
|
|
512
|
+
parse(expression) {
|
|
513
|
+
if (typeof expression !== 'string') throw new WhenSyntaxError('expression must be a string');
|
|
514
|
+
if (expression.length > this.maxLength) throw new WhenSyntaxError('expression too long');
|
|
515
|
+
return new Parser(tokenize(expression)).parse();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Evaluate a `when` expression against a variables map.
|
|
520
|
+
* @returns the evaluated value (commonly a boolean).
|
|
521
|
+
* @throws WhenSyntaxError on parse errors, WhenEvalError on evaluation errors.
|
|
522
|
+
*/
|
|
523
|
+
evaluate(expression, variables) {
|
|
524
|
+
const ast = this.parse(expression);
|
|
525
|
+
const scope = (variables && typeof variables === 'object') ? variables : {};
|
|
526
|
+
return evalNode(ast, scope, this.functions, 0);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Convenience: evaluate and coerce to boolean; returns null on any error. */
|
|
530
|
+
evaluateAsBoolean(expression, variables) {
|
|
531
|
+
try {
|
|
532
|
+
const v = this.evaluate(expression, variables);
|
|
533
|
+
if (v === null || v === undefined) return null;
|
|
534
|
+
return Boolean(v);
|
|
535
|
+
} catch (e) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
module.exports = { TiledeskWhenExpression, WhenSyntaxError, WhenEvalError };
|
|
@@ -391,15 +391,18 @@ class TiledeskChatbot {
|
|
|
391
391
|
// Store intent tracking data for downstream completion/block analytics
|
|
392
392
|
this._lastIntentId = answerObj.intent_id || answerObj._id?.toString() || '';
|
|
393
393
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
394
|
+
// Only track published (production) runs (root/draft copy has no root_id).
|
|
395
|
+
if (this.bot.root_id) {
|
|
396
|
+
AnalyticsClient.track('agent.intent_matched', this.projectId, {
|
|
397
|
+
agent_id: this.bot.root_id,
|
|
398
|
+
intent_id: answerObj.intent_id || answerObj._id?.toString() || '',
|
|
399
|
+
intent_name: intent_name,
|
|
400
|
+
match_type: matchContext.match_type || 'explicit',
|
|
401
|
+
confidence: (answerObj.score != null) ? answerObj.score : null,
|
|
402
|
+
step_count: _step,
|
|
403
|
+
request_id: this.requestId || null
|
|
404
|
+
});
|
|
405
|
+
}
|
|
403
406
|
|
|
404
407
|
const context = {
|
|
405
408
|
payload: {
|
package/index.js
CHANGED
|
@@ -187,10 +187,12 @@ router.post('/ext/:botid', async (req, res) => {
|
|
|
187
187
|
winston.error("(tybotRoute) Error while processing actions:", error);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
// Only track published (production) runs: the root/draft copy has no root_id,
|
|
191
|
+
// so draft/test executions are intentionally excluded from analytics.
|
|
192
|
+
if (chatbot._intentStartTime && bot.root_id) {
|
|
191
193
|
const intentDuration = Date.now() - chatbot._intentStartTime;
|
|
192
194
|
AnalyticsClient.track('agent.intent_completed', projectId, {
|
|
193
|
-
agent_id: bot.root_id
|
|
195
|
+
agent_id: bot.root_id,
|
|
194
196
|
intent_id: chatbot._lastIntentId || '',
|
|
195
197
|
intent_name: reply.attributes?.intent_info?.intent_name || 'unknown',
|
|
196
198
|
duration_ms: intentDuration,
|
|
@@ -217,7 +219,9 @@ router.post('/ext/:botid', async (req, res) => {
|
|
|
217
219
|
winston.verbose("(tybotRoute) sendSupportMessageExt reply sent: ", reply)
|
|
218
220
|
});
|
|
219
221
|
|
|
220
|
-
|
|
222
|
+
// Only track published (production) runs: the root/draft copy has no root_id,
|
|
223
|
+
// so draft/test executions are intentionally excluded from analytics.
|
|
224
|
+
if (chatbot._intentStartTime && bot.root_id) {
|
|
221
225
|
const intentDuration = Date.now() - chatbot._intentStartTime;
|
|
222
226
|
AnalyticsClient.track('agent.intent_completed', projectId, {
|
|
223
227
|
agent_id: bot.root_id,
|
|
@@ -593,12 +597,10 @@ router.post('/block/:project_id/:bot_id/:block_id', async (req, res) => {
|
|
|
593
597
|
const execution_id = uuidv4().replace(/-/g, '');
|
|
594
598
|
request_id = "automation-request-" + project_id + "-" + execution_id;
|
|
595
599
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
request_id: request_id
|
|
601
|
-
});
|
|
600
|
+
// webhook.triggered is emitted by tiledesk-server (routes/webhook.js) — the
|
|
601
|
+
// single source for production webhook automations (it carries webhook_id and
|
|
602
|
+
// excludes dev/draft runs). Not emitted here to avoid double-counting; the
|
|
603
|
+
// block execution itself is already recorded via agent.block_executed.
|
|
602
604
|
|
|
603
605
|
const command = "/#" + block_id;
|
|
604
606
|
let message = {
|