n8n-nodes-chatads 0.1.2 → 0.1.3
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 +21 -7
- package/dist/nodes/ChatAds/ChatAds.node.js +120 -130
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## ChatAds n8n Wrapper
|
|
2
2
|
|
|
3
|
-
Custom n8n node + credential that
|
|
3
|
+
Custom n8n node + credential that calls the ChatAds API endpoint `/v1/chatads/messages`. Handles authentication via the `x-api-key` header and exposes all supported API fields so you can orchestrate ChatAds inside n8n workflows.
|
|
4
4
|
|
|
5
5
|
### Layout
|
|
6
6
|
|
|
@@ -19,20 +19,34 @@ Custom n8n node + credential that call the FastAPI endpoint defined in `affiliat
|
|
|
19
19
|
```
|
|
20
20
|
This produces `dist/credentials/ChatAdsApi.credentials.js` and `dist/nodes/ChatAds/ChatAds.node.js`.
|
|
21
21
|
2. Copy the compiled `dist` directory into your n8n custom nodes directory (for example `~/.n8n/custom/`) or publish the package to your internal npm registry and install it where your n8n instance can resolve it.
|
|
22
|
-
3. Restart n8n. The new **ChatAds** node will appear under the
|
|
23
|
-
- A simple `message` plus optional fields (IP,
|
|
24
|
-
- A raw JSON payload
|
|
22
|
+
3. Restart n8n. The new **ChatAds** node will appear under the "Transform" group. Add it to a workflow, select the `ChatAds API` credential, and supply either:
|
|
23
|
+
- A simple `message` plus optional fields (IP, country, etc.), or
|
|
24
|
+
- A raw JSON payload (only documented fields are accepted; unexpected keys are rejected to prevent tampering).
|
|
25
25
|
4. Optionally tune `Max Concurrent Requests` (default 4) and `Request Timeout (seconds)` for high-volume workflows. The node keeps item ordering consistent even when issuing requests in parallel.
|
|
26
26
|
5. When executed, the node sends a POST request to `{{baseUrl}}/v1/chatads/messages` (configurable via the `Endpoint Override` parameter) with your `x-api-key` header and returns the FastAPI response verbatim so downstream nodes can branch on `success`, `error`, or any ad copy the backend generated.
|
|
27
27
|
|
|
28
|
-
Because the wrapper still uses `this.helpers.httpRequest`, it honors n8n
|
|
29
|
-
`Extra Fields (JSON)` is validated
|
|
28
|
+
Because the wrapper still uses `this.helpers.httpRequest`, it honors n8n's retry/backoff settings and the `Continue On Fail` toggle while layering per-node timeouts and error payloads for easier debugging.
|
|
29
|
+
`Extra Fields (JSON)` is validated to prevent conflicts with reserved parameter keys, so untrusted workflows cannot silently override core fields.
|
|
30
30
|
|
|
31
31
|
### Releasing/Patching
|
|
32
32
|
|
|
33
33
|
1. Bump the version in `package.json` whenever you change behavior (validation rules, new fields, etc.).
|
|
34
34
|
2. Run `npm run build` to regenerate `dist/` artifacts—**these compiled files are what n8n loads**, so they must stay in sync with the TypeScript sources.
|
|
35
35
|
3. Copy/publish the updated `dist/` directory to your custom nodes location or npm registry, then restart n8n.
|
|
36
|
-
4. If the backend `FunctionItem` schema changes, update both the parameter list in `ChatAds.node.ts` and the `
|
|
36
|
+
4. If the backend `FunctionItem` schema changes, update both the parameter list in `ChatAds.node.ts` and the `OPTIONAL_FIELDS` set near the top of the file to keep validation in sync.
|
|
37
37
|
|
|
38
38
|
Consider keeping a short changelog (GitHub release notes or a `CHANGELOG.md`) so downstream workflows know when to reinstall the node.
|
|
39
|
+
|
|
40
|
+
### Request Fields
|
|
41
|
+
|
|
42
|
+
The node accepts the following fields (via parameters or `Extra Fields (JSON)`):
|
|
43
|
+
|
|
44
|
+
| Field | Type | Description |
|
|
45
|
+
|-------|------|-------------|
|
|
46
|
+
| `message` | string (required) | Message to analyze (1-5000 chars) |
|
|
47
|
+
| `ip` | string | Client IP address for geo-detection |
|
|
48
|
+
| `country` | string | ISO 3166-1 alpha-2 country code for geo-targeting |
|
|
49
|
+
| `message_analysis` | string | Extraction method: `"fast"`, `"balanced"` (default), `"thorough"` |
|
|
50
|
+
| `fill_priority` | string | URL resolution: `"speed"` or `"coverage"` (default) |
|
|
51
|
+
| `min_intent` | string | Intent filter: `"any"`, `"low"` (default), `"medium"`, `"high"` |
|
|
52
|
+
| `skip_message_analysis` | boolean | Skip NLP/LLM and use message directly as search query |
|
|
@@ -3,25 +3,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ChatAds = void 0;
|
|
4
4
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
5
|
const DEFAULT_ENDPOINT = '/v1/chatads/messages';
|
|
6
|
-
const
|
|
7
|
-
'timestamp',
|
|
8
|
-
'pageUrl',
|
|
9
|
-
'pageTitle',
|
|
10
|
-
'referrer',
|
|
11
|
-
'address',
|
|
12
|
-
'email',
|
|
13
|
-
'type',
|
|
14
|
-
'domain',
|
|
15
|
-
'userAgent',
|
|
6
|
+
const OPTIONAL_FIELDS = new Set([
|
|
16
7
|
'ip',
|
|
17
|
-
'reason',
|
|
18
|
-
'company',
|
|
19
|
-
'name',
|
|
20
|
-
'message',
|
|
21
8
|
'country',
|
|
22
|
-
'
|
|
23
|
-
'
|
|
9
|
+
'message_analysis',
|
|
10
|
+
'fill_priority',
|
|
11
|
+
'min_intent',
|
|
12
|
+
'skip_message_analysis',
|
|
24
13
|
]);
|
|
14
|
+
const FIELD_ALIASES = {
|
|
15
|
+
messageanalysis: 'message_analysis',
|
|
16
|
+
message_analysis: 'message_analysis',
|
|
17
|
+
fillpriority: 'fill_priority',
|
|
18
|
+
fill_priority: 'fill_priority',
|
|
19
|
+
minintent: 'min_intent',
|
|
20
|
+
min_intent: 'min_intent',
|
|
21
|
+
skipmessageanalysis: 'skip_message_analysis',
|
|
22
|
+
skip_message_analysis: 'skip_message_analysis',
|
|
23
|
+
};
|
|
24
|
+
const RESERVED_PAYLOAD_KEYS = new Set(['message', ...OPTIONAL_FIELDS]);
|
|
25
25
|
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
26
26
|
const normalizeJsonPath = (path) => path.startsWith('/') ? path : `/${path}`;
|
|
27
27
|
const parseJsonObject = (context, value, fieldName, itemIndex) => {
|
|
@@ -52,20 +52,6 @@ const coerceToString = (context, source, key, value, itemIndex) => {
|
|
|
52
52
|
}
|
|
53
53
|
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `${source} field "${key}" must be a string or primitive`, { itemIndex });
|
|
54
54
|
};
|
|
55
|
-
const sanitizeRecord = (context, payload, source, itemIndex) => {
|
|
56
|
-
const sanitized = {};
|
|
57
|
-
for (const [key, value] of Object.entries(payload)) {
|
|
58
|
-
if (value === undefined || value === null) {
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
const normalized = coerceToString(context, source, key, value, itemIndex);
|
|
62
|
-
if (normalized === '') {
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
sanitized[key] = normalized;
|
|
66
|
-
}
|
|
67
|
-
return sanitized;
|
|
68
|
-
};
|
|
69
55
|
const ensureMessage = (context, value, itemIndex) => {
|
|
70
56
|
if (typeof value !== 'string') {
|
|
71
57
|
throw new n8n_workflow_1.NodeOperationError(context.getNode(), 'Field "message" must be provided as a string', { itemIndex });
|
|
@@ -76,6 +62,50 @@ const ensureMessage = (context, value, itemIndex) => {
|
|
|
76
62
|
}
|
|
77
63
|
return trimmed;
|
|
78
64
|
};
|
|
65
|
+
const normalizeFieldKey = (key) => { var _a; return (_a = FIELD_ALIASES[key.toLowerCase()]) !== null && _a !== void 0 ? _a : key; };
|
|
66
|
+
const normalizeOptionalField = (key) => {
|
|
67
|
+
const normalized = normalizeFieldKey(key);
|
|
68
|
+
if (OPTIONAL_FIELDS.has(normalized)) {
|
|
69
|
+
return normalized;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
};
|
|
73
|
+
const assertNoReservedConflicts = (context, source, extras, itemIndex) => {
|
|
74
|
+
const conflicts = Object.keys(extras).filter((key) => RESERVED_PAYLOAD_KEYS.has(normalizeFieldKey(key)));
|
|
75
|
+
if (conflicts.length > 0) {
|
|
76
|
+
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `${source} contains reserved keys: ${conflicts.join(', ')}`, { itemIndex });
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const buildPayloadFromObject = (context, input, source, itemIndex) => {
|
|
80
|
+
const payload = {};
|
|
81
|
+
const extras = {};
|
|
82
|
+
const message = ensureMessage(context, input.message, itemIndex);
|
|
83
|
+
payload.message = message;
|
|
84
|
+
for (const [rawKey, rawValue] of Object.entries(input)) {
|
|
85
|
+
if (rawKey === 'message') {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (rawValue === undefined || rawValue === null || rawValue === '') {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const optionalKey = normalizeOptionalField(rawKey);
|
|
92
|
+
if (optionalKey) {
|
|
93
|
+
if (optionalKey === 'skip_message_analysis') {
|
|
94
|
+
if (typeof rawValue !== 'boolean') {
|
|
95
|
+
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `${source} field "${rawKey}" must be a boolean`, { itemIndex });
|
|
96
|
+
}
|
|
97
|
+
payload[optionalKey] = rawValue;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
payload[optionalKey] = coerceToString(context, source, rawKey, rawValue, itemIndex);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
extras[rawKey] = rawValue;
|
|
104
|
+
}
|
|
105
|
+
assertNoReservedConflicts(context, source, extras, itemIndex);
|
|
106
|
+
Object.assign(payload, extras);
|
|
107
|
+
return payload;
|
|
108
|
+
};
|
|
79
109
|
const buildErrorPayload = (error) => {
|
|
80
110
|
const fallback = {
|
|
81
111
|
message: 'Unknown error',
|
|
@@ -102,13 +132,6 @@ const buildErrorPayload = (error) => {
|
|
|
102
132
|
}
|
|
103
133
|
return fallback;
|
|
104
134
|
};
|
|
105
|
-
const assertAllowedFields = (context, source, payload, allowed, itemIndex) => {
|
|
106
|
-
for (const key of Object.keys(payload)) {
|
|
107
|
-
if (!allowed.has(key)) {
|
|
108
|
-
throw new n8n_workflow_1.NodeOperationError(context.getNode(), `${source} field "${key}" is not supported`, { itemIndex });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
135
|
class ChatAds {
|
|
113
136
|
constructor() {
|
|
114
137
|
this.description = {
|
|
@@ -192,102 +215,62 @@ class ChatAds {
|
|
|
192
215
|
},
|
|
193
216
|
},
|
|
194
217
|
options: [
|
|
195
|
-
{
|
|
196
|
-
displayName: 'Timestamp',
|
|
197
|
-
name: 'timestamp',
|
|
198
|
-
type: 'string',
|
|
199
|
-
default: '',
|
|
200
|
-
description: 'ISO timestamp when the interaction occurred',
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
displayName: 'Page URL',
|
|
204
|
-
name: 'pageUrl',
|
|
205
|
-
type: 'string',
|
|
206
|
-
default: '',
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
displayName: 'Page Title',
|
|
210
|
-
name: 'pageTitle',
|
|
211
|
-
type: 'string',
|
|
212
|
-
default: '',
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
displayName: 'Referrer',
|
|
216
|
-
name: 'referrer',
|
|
217
|
-
type: 'string',
|
|
218
|
-
default: '',
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
displayName: 'Address',
|
|
222
|
-
name: 'address',
|
|
223
|
-
type: 'string',
|
|
224
|
-
default: '',
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
displayName: 'Email',
|
|
228
|
-
name: 'email',
|
|
229
|
-
type: 'string',
|
|
230
|
-
default: '',
|
|
231
|
-
},
|
|
232
|
-
{
|
|
233
|
-
displayName: 'Type',
|
|
234
|
-
name: 'type',
|
|
235
|
-
type: 'string',
|
|
236
|
-
default: '',
|
|
237
|
-
},
|
|
238
|
-
{
|
|
239
|
-
displayName: 'Domain',
|
|
240
|
-
name: 'domain',
|
|
241
|
-
type: 'string',
|
|
242
|
-
default: '',
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
displayName: 'User Agent',
|
|
246
|
-
name: 'userAgent',
|
|
247
|
-
type: 'string',
|
|
248
|
-
default: '',
|
|
249
|
-
},
|
|
250
218
|
{
|
|
251
219
|
displayName: 'IP Address',
|
|
252
220
|
name: 'ip',
|
|
253
221
|
type: 'string',
|
|
254
222
|
default: '',
|
|
223
|
+
description: 'Client IP address for geo-detection',
|
|
255
224
|
},
|
|
256
225
|
{
|
|
257
|
-
displayName: '
|
|
258
|
-
name: '
|
|
259
|
-
type: 'string',
|
|
260
|
-
default: '',
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
displayName: 'Company',
|
|
264
|
-
name: 'company',
|
|
226
|
+
displayName: 'Country',
|
|
227
|
+
name: 'country',
|
|
265
228
|
type: 'string',
|
|
266
229
|
default: '',
|
|
230
|
+
description: 'ISO 3166-1 alpha-2 country code for geo-targeting',
|
|
267
231
|
},
|
|
268
232
|
{
|
|
269
|
-
displayName: '
|
|
270
|
-
name: '
|
|
271
|
-
type: '
|
|
272
|
-
|
|
233
|
+
displayName: 'Message Analysis',
|
|
234
|
+
name: 'message_analysis',
|
|
235
|
+
type: 'options',
|
|
236
|
+
options: [
|
|
237
|
+
{ name: 'Fast', value: 'fast' },
|
|
238
|
+
{ name: 'Balanced', value: 'balanced' },
|
|
239
|
+
{ name: 'Thorough', value: 'thorough' },
|
|
240
|
+
],
|
|
241
|
+
default: 'balanced',
|
|
242
|
+
description: 'Keyword extraction method. Fast uses NLP only (~50ms), others use LLM.',
|
|
273
243
|
},
|
|
274
244
|
{
|
|
275
|
-
displayName: '
|
|
276
|
-
name: '
|
|
277
|
-
type: '
|
|
278
|
-
|
|
245
|
+
displayName: 'Fill Priority',
|
|
246
|
+
name: 'fill_priority',
|
|
247
|
+
type: 'options',
|
|
248
|
+
options: [
|
|
249
|
+
{ name: 'Speed', value: 'speed' },
|
|
250
|
+
{ name: 'Coverage', value: 'coverage' },
|
|
251
|
+
],
|
|
252
|
+
default: 'coverage',
|
|
253
|
+
description: 'URL resolution fallback strategy',
|
|
279
254
|
},
|
|
280
255
|
{
|
|
281
|
-
displayName: '
|
|
282
|
-
name: '
|
|
283
|
-
type: '
|
|
284
|
-
|
|
256
|
+
displayName: 'Min Intent',
|
|
257
|
+
name: 'min_intent',
|
|
258
|
+
type: 'options',
|
|
259
|
+
options: [
|
|
260
|
+
{ name: 'Any', value: 'any' },
|
|
261
|
+
{ name: 'Low', value: 'low' },
|
|
262
|
+
{ name: 'Medium', value: 'medium' },
|
|
263
|
+
{ name: 'High', value: 'high' },
|
|
264
|
+
],
|
|
265
|
+
default: 'low',
|
|
266
|
+
description: 'Minimum purchase intent level for affiliate resolution',
|
|
285
267
|
},
|
|
286
268
|
{
|
|
287
|
-
displayName: '
|
|
288
|
-
name: '
|
|
289
|
-
type: '
|
|
290
|
-
default:
|
|
269
|
+
displayName: 'Skip Message Analysis',
|
|
270
|
+
name: 'skip_message_analysis',
|
|
271
|
+
type: 'boolean',
|
|
272
|
+
default: false,
|
|
273
|
+
description: 'Skip NLP/LLM extraction and use message directly as search query. Overrides message_analysis, min_intent, and fill_priority.',
|
|
291
274
|
},
|
|
292
275
|
{
|
|
293
276
|
displayName: 'Extra Fields (JSON)',
|
|
@@ -297,7 +280,7 @@ class ChatAds {
|
|
|
297
280
|
alwaysOpenEditWindow: true,
|
|
298
281
|
},
|
|
299
282
|
default: '{}',
|
|
300
|
-
description: '
|
|
283
|
+
description: 'JSON object containing additional fields to merge into the payload',
|
|
301
284
|
},
|
|
302
285
|
],
|
|
303
286
|
},
|
|
@@ -364,38 +347,45 @@ class ChatAds {
|
|
|
364
347
|
if (jsonParameters) {
|
|
365
348
|
const bodyJson = this.getNodeParameter('bodyJson', itemIndex);
|
|
366
349
|
const parsedBody = parseJsonObject(this, bodyJson, 'Body JSON', itemIndex);
|
|
367
|
-
|
|
368
|
-
const message = ensureMessage(this, parsedBody.message, itemIndex);
|
|
369
|
-
sanitizedBody.message = message;
|
|
370
|
-
assertAllowedFields(this, 'Body JSON', sanitizedBody, ALLOWED_PAYLOAD_FIELDS, itemIndex);
|
|
371
|
-
body = sanitizedBody;
|
|
350
|
+
body = buildPayloadFromObject(this, parsedBody, 'Body JSON', itemIndex);
|
|
372
351
|
}
|
|
373
352
|
else {
|
|
374
353
|
const rawMessage = this.getNodeParameter('message', itemIndex);
|
|
375
354
|
const message = ensureMessage(this, rawMessage, itemIndex);
|
|
376
355
|
const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {});
|
|
377
|
-
|
|
356
|
+
const constructed = { message };
|
|
378
357
|
for (const [field, value] of Object.entries(additionalFields)) {
|
|
379
358
|
if (value === undefined || value === null || value === '') {
|
|
380
359
|
continue;
|
|
381
360
|
}
|
|
382
361
|
if (field === 'extraFieldsJson') {
|
|
383
362
|
const parsedExtras = parseJsonObject(this, value, 'Extra fields JSON', itemIndex);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
363
|
+
assertNoReservedConflicts(this, 'Extra fields JSON', parsedExtras, itemIndex);
|
|
364
|
+
for (const [extraKey, extraValue] of Object.entries(parsedExtras)) {
|
|
365
|
+
if (extraValue === undefined || extraValue === null) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
387
368
|
if (extraKey === 'message') {
|
|
388
369
|
continue;
|
|
389
370
|
}
|
|
390
|
-
|
|
371
|
+
constructed[extraKey] = extraValue;
|
|
391
372
|
}
|
|
392
373
|
continue;
|
|
393
374
|
}
|
|
394
|
-
|
|
395
|
-
|
|
375
|
+
const normalizedKey = normalizeOptionalField(field);
|
|
376
|
+
if (!normalizedKey) {
|
|
377
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Field "${field}" is not supported`, { itemIndex });
|
|
378
|
+
}
|
|
379
|
+
if (normalizedKey === 'skip_message_analysis') {
|
|
380
|
+
if (typeof value !== 'boolean') {
|
|
381
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Field "${field}" must be provided as a boolean`, { itemIndex });
|
|
382
|
+
}
|
|
383
|
+
constructed[normalizedKey] = value;
|
|
384
|
+
continue;
|
|
396
385
|
}
|
|
397
|
-
|
|
386
|
+
constructed[normalizedKey] = coerceToString(this, 'Additional fields', field, value, itemIndex);
|
|
398
387
|
}
|
|
388
|
+
body = buildPayloadFromObject(this, constructed, 'Additional fields', itemIndex);
|
|
399
389
|
}
|
|
400
390
|
const response = await this.helpers.httpRequestWithAuthentication.call(this, 'chatAdsApi', {
|
|
401
391
|
method: 'POST',
|