n8n-nodes-dopomogai 0.1.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 ADDED
@@ -0,0 +1,28 @@
1
+ # n8n-nodes-dopomogai
2
+
3
+ Community nodes for DopomogAI.
4
+
5
+ ## Base URL
6
+ Use:
7
+ - `https://api.dopomogai.com`
8
+
9
+ The node will call:
10
+ - `${apiBaseUrl}/api/v1/...`
11
+
12
+ ## Auth
13
+ Use an API key (`pk_...` or `sk_...`) via:
14
+ - `Authorization: Bearer <key>` (default)
15
+ or
16
+ - `X-API-Key: <key>`
17
+
18
+ ## Included
19
+ - Agents CRUD
20
+ - Conversations management
21
+ - Chat (blocking, SSE internally)
22
+ - Files upload + metadata
23
+
24
+ ## Dev
25
+ ```bash
26
+ npm i
27
+ npm run build
28
+ ```
@@ -0,0 +1,7 @@
1
+ import type { ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class DopomogaiApiKey implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DopomogaiApiKey = void 0;
4
+ class DopomogaiApiKey {
5
+ name = 'dopomogaiApiKey';
6
+ displayName = 'DopomogAI API Key';
7
+ documentationUrl = 'https://api.dopomogai.com/api/docs';
8
+ properties = [
9
+ {
10
+ displayName: 'API Base URL',
11
+ name: 'apiBaseUrl',
12
+ type: 'string',
13
+ default: 'https://api.dopomogai.com',
14
+ placeholder: 'https://api.dopomogai.com',
15
+ required: true,
16
+ description: 'Base URL of DopomogAI API (no trailing slash).',
17
+ },
18
+ {
19
+ displayName: 'API Key',
20
+ name: 'apiKey',
21
+ type: 'string',
22
+ typeOptions: { password: true },
23
+ default: '',
24
+ required: true,
25
+ description: 'Your DopomogAI API key (pk_... or sk_...).',
26
+ },
27
+ {
28
+ displayName: 'Send API Key As',
29
+ name: 'sendAs',
30
+ type: 'options',
31
+ default: 'authorizationBearer',
32
+ options: [
33
+ { name: 'Authorization: Bearer <key>', value: 'authorizationBearer' },
34
+ { name: 'X-API-Key: <key>', value: 'xApiKey' },
35
+ ],
36
+ },
37
+ ];
38
+ }
39
+ exports.DopomogaiApiKey = DopomogaiApiKey;
@@ -0,0 +1,2 @@
1
+ export * from './nodes/Dopomogai/Dopomogai.node';
2
+ export * from './credentials/DopomogaiApiKey.credentials';
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./nodes/Dopomogai/Dopomogai.node"), exports);
18
+ __exportStar(require("./credentials/DopomogaiApiKey.credentials"), exports);
@@ -0,0 +1,5 @@
1
+ import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class Dopomogai implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,496 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Dopomogai = void 0;
4
+ const http_1 = require("./helpers/http");
5
+ const sse_1 = require("./helpers/sse");
6
+ class Dopomogai {
7
+ description = {
8
+ displayName: 'DopomogAI',
9
+ name: 'dopomogai',
10
+ icon: 'file:dopomogai.svg',
11
+ group: ['transform'],
12
+ version: 1,
13
+ description: 'Interact with DopomogAI backend (Agents, Conversations, Chat, Files).',
14
+ defaults: { name: 'DopomogAI' },
15
+ inputs: ['main'],
16
+ outputs: ['main'],
17
+ credentials: [{ name: 'dopomogaiApiKey', required: true }],
18
+ properties: [
19
+ {
20
+ displayName: 'Resource',
21
+ name: 'resource',
22
+ type: 'options',
23
+ noDataExpression: true,
24
+ default: 'agents',
25
+ options: [
26
+ { name: 'Agents', value: 'agents' },
27
+ { name: 'Conversations', value: 'conversations' },
28
+ { name: 'Chat', value: 'chat' },
29
+ { name: 'Files', value: 'files' },
30
+ ],
31
+ },
32
+ // -------------------- Agents --------------------
33
+ {
34
+ displayName: 'Operation',
35
+ name: 'operation',
36
+ type: 'options',
37
+ noDataExpression: true,
38
+ default: 'list',
39
+ displayOptions: { show: { resource: ['agents'] } },
40
+ options: [
41
+ { name: 'List', value: 'list' },
42
+ { name: 'Create', value: 'create' },
43
+ { name: 'Get', value: 'get' },
44
+ { name: 'Update', value: 'update' },
45
+ { name: 'Delete', value: 'delete' },
46
+ ],
47
+ },
48
+ {
49
+ displayName: 'Agent ID',
50
+ name: 'agentId',
51
+ type: 'string',
52
+ default: '',
53
+ displayOptions: { show: { resource: ['agents'], operation: ['get', 'update', 'delete'] } },
54
+ },
55
+ {
56
+ displayName: 'Name',
57
+ name: 'agentName',
58
+ type: 'string',
59
+ default: '',
60
+ displayOptions: { show: { resource: ['agents'], operation: ['create'] } },
61
+ required: true,
62
+ },
63
+ {
64
+ displayName: 'LLM Model',
65
+ name: 'llmModel',
66
+ type: 'string',
67
+ default: 'openrouter:openai/gpt-4o',
68
+ displayOptions: { show: { resource: ['agents'], operation: ['create'] } },
69
+ required: true,
70
+ },
71
+ {
72
+ displayName: 'Description',
73
+ name: 'agentDescription',
74
+ type: 'string',
75
+ default: '',
76
+ displayOptions: { show: { resource: ['agents'], operation: ['create', 'update'] } },
77
+ },
78
+ {
79
+ displayName: 'Is Public',
80
+ name: 'agentIsPublic',
81
+ type: 'boolean',
82
+ default: false,
83
+ displayOptions: { show: { resource: ['agents'], operation: ['create', 'update'] } },
84
+ },
85
+ {
86
+ displayName: 'Agent Settings (JSON)',
87
+ name: 'agentSettingsJson',
88
+ type: 'string',
89
+ default: '{}',
90
+ description: 'JSON for agent_settings. For update we replace/merge in backend depending on your API behavior.',
91
+ displayOptions: { show: { resource: ['agents'], operation: ['create', 'update'] } },
92
+ },
93
+ // -------------------- Conversations --------------------
94
+ {
95
+ displayName: 'Operation',
96
+ name: 'conversationOperation',
97
+ type: 'options',
98
+ noDataExpression: true,
99
+ default: 'list',
100
+ displayOptions: { show: { resource: ['conversations'] } },
101
+ options: [
102
+ { name: 'List', value: 'list' },
103
+ { name: 'Get', value: 'get' },
104
+ { name: 'Rename', value: 'rename' },
105
+ { name: 'Delete', value: 'delete' },
106
+ ],
107
+ },
108
+ {
109
+ displayName: 'Conversation ID',
110
+ name: 'conversationId',
111
+ type: 'string',
112
+ default: '',
113
+ displayOptions: { show: { resource: ['conversations'], conversationOperation: ['get', 'rename', 'delete'] } },
114
+ required: true,
115
+ },
116
+ {
117
+ displayName: 'Title',
118
+ name: 'conversationTitle',
119
+ type: 'string',
120
+ default: '',
121
+ displayOptions: { show: { resource: ['conversations'], conversationOperation: ['rename'] } },
122
+ required: true,
123
+ },
124
+ {
125
+ displayName: 'List Options',
126
+ name: 'conversationListOptions',
127
+ type: 'collection',
128
+ placeholder: 'Add option',
129
+ default: {},
130
+ displayOptions: { show: { resource: ['conversations'], conversationOperation: ['list'] } },
131
+ options: [
132
+ { displayName: 'Limit', name: 'limit', type: 'number', default: 20 },
133
+ { displayName: 'Offset', name: 'offset', type: 'number', default: 0 },
134
+ { displayName: 'Agent ID', name: 'agent_id', type: 'string', default: '' },
135
+ { displayName: 'Search Query', name: 'search_query', type: 'string', default: '' },
136
+ { displayName: 'Sort By', name: 'sort_by', type: 'string', default: 'updated_at' },
137
+ { displayName: 'Sort Order', name: 'sort_order', type: 'options', default: 'desc', options: [
138
+ { name: 'Desc', value: 'desc' },
139
+ { name: 'Asc', value: 'asc' },
140
+ ] },
141
+ ],
142
+ },
143
+ // -------------------- Chat --------------------
144
+ {
145
+ displayName: 'Operation',
146
+ name: 'chatOperation',
147
+ type: 'options',
148
+ noDataExpression: true,
149
+ default: 'chatBlocking',
150
+ displayOptions: { show: { resource: ['chat'] } },
151
+ options: [
152
+ { name: 'Chat (Blocking)', value: 'chatBlocking' },
153
+ ],
154
+ },
155
+ {
156
+ displayName: 'Agent ID',
157
+ name: 'chatAgentId',
158
+ type: 'string',
159
+ default: '',
160
+ displayOptions: { show: { resource: ['chat'], chatOperation: ['chatBlocking'] } },
161
+ required: true,
162
+ description: 'Agent UUID',
163
+ },
164
+ {
165
+ displayName: 'Conversation ID',
166
+ name: 'chatConversationId',
167
+ type: 'string',
168
+ default: '',
169
+ displayOptions: { show: { resource: ['chat'], chatOperation: ['chatBlocking'] } },
170
+ description: 'Optional. If omitted, backend will create one and return it via X-Conversation-Id.',
171
+ },
172
+ {
173
+ displayName: 'User Message',
174
+ name: 'chatUserMessage',
175
+ type: 'string',
176
+ default: '',
177
+ displayOptions: { show: { resource: ['chat'], chatOperation: ['chatBlocking'] } },
178
+ description: 'Text message to send to the agent.',
179
+ },
180
+ {
181
+ displayName: 'Initial Context',
182
+ name: 'chatInitialContext',
183
+ type: 'string',
184
+ default: '',
185
+ displayOptions: { show: { resource: ['chat'], chatOperation: ['chatBlocking'] } },
186
+ },
187
+ {
188
+ displayName: 'Turn Prompt Override',
189
+ name: 'chatTurnPromptOverride',
190
+ type: 'string',
191
+ default: '',
192
+ displayOptions: { show: { resource: ['chat'], chatOperation: ['chatBlocking'] } },
193
+ },
194
+ {
195
+ displayName: 'Parts (JSON)',
196
+ name: 'chatPartsJson',
197
+ type: 'string',
198
+ default: '[]',
199
+ displayOptions: { show: { resource: ['chat'], chatOperation: ['chatBlocking'] } },
200
+ description: 'Optional parts array JSON (ConversationMessagePartInput). Example: [{"type":"file","file_id":"...","media_type":"application/pdf"}]',
201
+ },
202
+ {
203
+ displayName: 'Run Overrides (JSON)',
204
+ name: 'chatRunOverridesJson',
205
+ type: 'string',
206
+ default: '{}',
207
+ displayOptions: { show: { resource: ['chat'], chatOperation: ['chatBlocking'] } },
208
+ description: 'Optional run_overrides JSON.',
209
+ },
210
+ {
211
+ displayName: 'Return Full Event Log',
212
+ name: 'chatReturnEvents',
213
+ type: 'boolean',
214
+ default: false,
215
+ displayOptions: { show: { resource: ['chat'], chatOperation: ['chatBlocking'] } },
216
+ },
217
+ // -------------------- Files --------------------
218
+ {
219
+ displayName: 'Operation',
220
+ name: 'fileOperation',
221
+ type: 'options',
222
+ noDataExpression: true,
223
+ default: 'upload',
224
+ displayOptions: { show: { resource: ['files'] } },
225
+ options: [
226
+ { name: 'Upload', value: 'upload' },
227
+ { name: 'Get Metadata', value: 'getMetadata' },
228
+ ],
229
+ },
230
+ {
231
+ displayName: 'Binary Property',
232
+ name: 'binaryProperty',
233
+ type: 'string',
234
+ default: 'data',
235
+ displayOptions: { show: { resource: ['files'], fileOperation: ['upload'] } },
236
+ description: 'Name of the binary property on the incoming item (default: data).',
237
+ },
238
+ {
239
+ displayName: 'Purpose',
240
+ name: 'filePurpose',
241
+ type: 'string',
242
+ default: 'agent_input',
243
+ displayOptions: { show: { resource: ['files'], fileOperation: ['upload'] } },
244
+ description: 'E.g. agent_input, knowledge_source, user_upload, general, ...',
245
+ },
246
+ {
247
+ displayName: 'Agent ID (Query)',
248
+ name: 'fileAgentId',
249
+ type: 'string',
250
+ default: '',
251
+ displayOptions: { show: { resource: ['files'], fileOperation: ['upload'] } },
252
+ description: 'Optional query param agent_id for optimized processing.',
253
+ },
254
+ {
255
+ displayName: 'Logical Path',
256
+ name: 'fileLogicalPath',
257
+ type: 'string',
258
+ default: '',
259
+ displayOptions: { show: { resource: ['files'], fileOperation: ['upload'] } },
260
+ },
261
+ {
262
+ displayName: 'SHA256',
263
+ name: 'fileSha256',
264
+ type: 'string',
265
+ default: '',
266
+ displayOptions: { show: { resource: ['files'], fileOperation: ['upload'] } },
267
+ },
268
+ {
269
+ displayName: 'File ID',
270
+ name: 'fileId',
271
+ type: 'string',
272
+ default: '',
273
+ displayOptions: { show: { resource: ['files'], fileOperation: ['getMetadata'] } },
274
+ required: true,
275
+ },
276
+ ],
277
+ };
278
+ async execute() {
279
+ const items = this.getInputData();
280
+ const out = [];
281
+ for (let i = 0; i < items.length; i++) {
282
+ const resource = this.getNodeParameter('resource', i);
283
+ // -------- Agents --------
284
+ if (resource === 'agents') {
285
+ const op = this.getNodeParameter('operation', i);
286
+ if (op === 'list') {
287
+ const { data } = await (0, http_1.dopomogaiRequest)(this, { method: 'GET', path: '/api/v1/agents' });
288
+ out.push({ json: data });
289
+ }
290
+ if (op === 'create') {
291
+ const name = this.getNodeParameter('agentName', i);
292
+ const llm_model = this.getNodeParameter('llmModel', i);
293
+ const description = this.getNodeParameter('agentDescription', i, '');
294
+ const is_public = this.getNodeParameter('agentIsPublic', i, false);
295
+ const settingsJson = this.getNodeParameter('agentSettingsJson', i, '{}');
296
+ let agent_settings = undefined;
297
+ try {
298
+ agent_settings = settingsJson ? JSON.parse(settingsJson) : undefined;
299
+ }
300
+ catch {
301
+ throw new Error('Invalid Agent Settings JSON');
302
+ }
303
+ const body = { name, llm_model, description: description || null, agent_settings: agent_settings || null, is_public };
304
+ const { data } = await (0, http_1.dopomogaiRequest)(this, { method: 'POST', path: '/api/v1/agents', body });
305
+ out.push({ json: data });
306
+ }
307
+ if (op === 'get') {
308
+ const agentId = this.getNodeParameter('agentId', i);
309
+ const { data } = await (0, http_1.dopomogaiRequest)(this, { method: 'GET', path: `/api/v1/agents/${agentId}` });
310
+ out.push({ json: data });
311
+ }
312
+ if (op === 'update') {
313
+ const agentId = this.getNodeParameter('agentId', i);
314
+ const description = this.getNodeParameter('agentDescription', i, '');
315
+ const is_public = this.getNodeParameter('agentIsPublic', i, false);
316
+ const settingsJson = this.getNodeParameter('agentSettingsJson', i, '{}');
317
+ let agent_settings = undefined;
318
+ try {
319
+ agent_settings = settingsJson ? JSON.parse(settingsJson) : undefined;
320
+ }
321
+ catch {
322
+ throw new Error('Invalid Agent Settings JSON');
323
+ }
324
+ const body = {};
325
+ if (description !== '')
326
+ body.description = description;
327
+ body.is_public = is_public;
328
+ if (agent_settings !== undefined)
329
+ body.agent_settings = agent_settings;
330
+ const { data } = await (0, http_1.dopomogaiRequest)(this, { method: 'PUT', path: `/api/v1/agents/${agentId}`, body });
331
+ out.push({ json: data });
332
+ }
333
+ if (op === 'delete') {
334
+ const agentId = this.getNodeParameter('agentId', i);
335
+ await (0, http_1.dopomogaiRequest)(this, { method: 'DELETE', path: `/api/v1/agents/${agentId}` });
336
+ out.push({ json: { ok: true } });
337
+ }
338
+ }
339
+ // -------- Conversations --------
340
+ if (resource === 'conversations') {
341
+ const op = this.getNodeParameter('conversationOperation', i);
342
+ if (op === 'list') {
343
+ const options = this.getNodeParameter('conversationListOptions', i, {});
344
+ const { data } = await (0, http_1.dopomogaiRequest)(this, {
345
+ method: 'GET',
346
+ path: '/api/v1/conversations',
347
+ query: options,
348
+ });
349
+ out.push({ json: data });
350
+ }
351
+ if (op === 'get') {
352
+ const conversationId = this.getNodeParameter('conversationId', i);
353
+ const { data } = await (0, http_1.dopomogaiRequest)(this, {
354
+ method: 'GET',
355
+ path: `/api/v1/conversations/${conversationId}`,
356
+ });
357
+ out.push({ json: data });
358
+ }
359
+ if (op === 'rename') {
360
+ const conversationId = this.getNodeParameter('conversationId', i);
361
+ const title = this.getNodeParameter('conversationTitle', i);
362
+ const { data } = await (0, http_1.dopomogaiRequest)(this, {
363
+ method: 'PATCH',
364
+ path: `/api/v1/conversations/${conversationId}/meta`,
365
+ body: { title },
366
+ });
367
+ out.push({ json: data });
368
+ }
369
+ if (op === 'delete') {
370
+ const conversationId = this.getNodeParameter('conversationId', i);
371
+ await (0, http_1.dopomogaiRequest)(this, {
372
+ method: 'DELETE',
373
+ path: `/api/v1/conversations/${conversationId}`,
374
+ });
375
+ out.push({ json: { ok: true } });
376
+ }
377
+ }
378
+ // -------- Chat --------
379
+ if (resource === 'chat') {
380
+ const op = this.getNodeParameter('chatOperation', i);
381
+ if (op !== 'chatBlocking')
382
+ throw new Error(`Unsupported chat operation: ${op}`);
383
+ const creds = (0, http_1.getCreds)(this);
384
+ const agentId = this.getNodeParameter('chatAgentId', i);
385
+ const conversationId = this.getNodeParameter('chatConversationId', i, '');
386
+ const user_message = this.getNodeParameter('chatUserMessage', i, '');
387
+ const initial_context = this.getNodeParameter('chatInitialContext', i, '');
388
+ const turn_prompt_override = this.getNodeParameter('chatTurnPromptOverride', i, '');
389
+ const partsJson = this.getNodeParameter('chatPartsJson', i, '[]');
390
+ const runOverridesJson = this.getNodeParameter('chatRunOverridesJson', i, '{}');
391
+ const returnEvents = this.getNodeParameter('chatReturnEvents', i, false);
392
+ let parts = undefined;
393
+ let run_overrides = undefined;
394
+ try {
395
+ parts = partsJson ? JSON.parse(partsJson) : undefined;
396
+ }
397
+ catch {
398
+ throw new Error('Invalid Parts JSON');
399
+ }
400
+ try {
401
+ run_overrides = runOverridesJson ? JSON.parse(runOverridesJson) : undefined;
402
+ }
403
+ catch {
404
+ throw new Error('Invalid Run Overrides JSON');
405
+ }
406
+ const body = {
407
+ conversation_id: conversationId || undefined,
408
+ user_message: user_message || undefined,
409
+ parts: Array.isArray(parts) && parts.length ? parts : undefined,
410
+ initial_context: initial_context || undefined,
411
+ turn_prompt_override: turn_prompt_override || undefined,
412
+ run_overrides: run_overrides && Object.keys(run_overrides).length ? run_overrides : undefined,
413
+ };
414
+ const url = `${creds.apiBaseUrl}/api/v1/agents/${agentId}/conversations/stream`;
415
+ const headers = (0, http_1.buildHeaders)(creds);
416
+ const sse = await (0, sse_1.postSseAndCollect)({
417
+ url,
418
+ headers,
419
+ body,
420
+ stopOn: { streamEnd: true, complete: false },
421
+ timeoutMs: 10 * 60 * 1000,
422
+ });
423
+ const finalText = sse.finalResponse || sse.accumulatedText;
424
+ out.push({
425
+ json: {
426
+ conversation_id: sse.conversationId || conversationId || null,
427
+ final_response: finalText || '',
428
+ usage: sse.usage,
429
+ events: returnEvents ? sse.events : undefined,
430
+ },
431
+ });
432
+ }
433
+ // -------- Files --------
434
+ if (resource === 'files') {
435
+ const op = this.getNodeParameter('fileOperation', i);
436
+ const creds = (0, http_1.getCreds)(this);
437
+ if (op === 'getMetadata') {
438
+ const fileId = this.getNodeParameter('fileId', i);
439
+ const { data } = await (0, http_1.dopomogaiRequest)(this, { method: 'GET', path: `/api/v1/files/${fileId}` });
440
+ out.push({
441
+ json: {
442
+ ...data,
443
+ download_url: `${creds.apiBaseUrl}/api/v1/files/${fileId}/download`,
444
+ },
445
+ });
446
+ }
447
+ if (op === 'upload') {
448
+ const binaryProperty = this.getNodeParameter('binaryProperty', i, 'data');
449
+ const purpose = this.getNodeParameter('filePurpose', i, 'agent_input');
450
+ const agentId = this.getNodeParameter('fileAgentId', i, '');
451
+ const logicalPath = this.getNodeParameter('fileLogicalPath', i, '');
452
+ const sha256 = this.getNodeParameter('fileSha256', i, '');
453
+ const bin = this.helpers.assertBinaryData(i, binaryProperty);
454
+ const buffer = await this.helpers.getBinaryDataBuffer(i, binaryProperty);
455
+ const filename = bin.fileName || 'upload.bin';
456
+ const mimeType = bin.mimeType || 'application/octet-stream';
457
+ const form = new FormData();
458
+ const bytes = new Uint8Array(buffer);
459
+ const file = new File([bytes], filename, { type: mimeType });
460
+ form.append('file', file);
461
+ form.append('purpose', purpose);
462
+ if (logicalPath)
463
+ form.append('logical_path', logicalPath);
464
+ if (sha256)
465
+ form.append('sha256', sha256);
466
+ const url = new URL(`${creds.apiBaseUrl}/api/v1/files/upload`);
467
+ if (agentId)
468
+ url.searchParams.set('agent_id', agentId);
469
+ const headers = (0, http_1.buildHeaders)(creds, {
470
+ // Let fetch set correct multipart boundary automatically; do not set Content-Type manually.
471
+ Accept: 'application/json',
472
+ });
473
+ const res = await fetch(url.toString(), {
474
+ method: 'POST',
475
+ headers,
476
+ body: form,
477
+ });
478
+ const payload = await res.json().catch(() => null);
479
+ if (!res.ok) {
480
+ const msg = payload?.detail || payload?.message || `HTTP ${res.status}`;
481
+ throw new Error(`Upload failed (${res.status}): ${msg}`);
482
+ }
483
+ const fileId = payload?.id;
484
+ out.push({
485
+ json: {
486
+ ...payload,
487
+ download_url: fileId ? `${creds.apiBaseUrl}/api/v1/files/${fileId}/download` : null,
488
+ },
489
+ });
490
+ }
491
+ }
492
+ }
493
+ return [out];
494
+ }
495
+ }
496
+ exports.Dopomogai = Dopomogai;
@@ -0,0 +1,19 @@
1
+ import type { IExecuteFunctions } from 'n8n-workflow';
2
+ export type DopomogaiCreds = {
3
+ apiBaseUrl: string;
4
+ apiKey: string;
5
+ sendAs: 'authorizationBearer' | 'xApiKey';
6
+ };
7
+ export declare function getCreds(ctx: IExecuteFunctions): DopomogaiCreds;
8
+ export declare function buildHeaders(creds: DopomogaiCreds, extra?: Record<string, string>): Record<string, string>;
9
+ export declare function dopomogaiRequest<T>(ctx: IExecuteFunctions, opts: {
10
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
11
+ path: string;
12
+ query?: Record<string, any>;
13
+ body?: any;
14
+ headers?: Record<string, string>;
15
+ }): Promise<{
16
+ data: T;
17
+ status: number;
18
+ headers: Headers;
19
+ }>;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getCreds = getCreds;
4
+ exports.buildHeaders = buildHeaders;
5
+ exports.dopomogaiRequest = dopomogaiRequest;
6
+ function getCreds(ctx) {
7
+ const c = ctx.getCredentials('dopomogaiApiKey');
8
+ if (!c?.apiBaseUrl)
9
+ throw new Error('Missing credentials.apiBaseUrl');
10
+ if (!c?.apiKey)
11
+ throw new Error('Missing credentials.apiKey');
12
+ return {
13
+ apiBaseUrl: String(c.apiBaseUrl).replace(/\/+$/, ''),
14
+ apiKey: String(c.apiKey),
15
+ sendAs: (c.sendAs || 'authorizationBearer'),
16
+ };
17
+ }
18
+ function buildHeaders(creds, extra) {
19
+ const headers = {
20
+ Accept: 'application/json',
21
+ ...(extra || {}),
22
+ };
23
+ if (creds.sendAs === 'xApiKey') {
24
+ headers['X-API-Key'] = creds.apiKey;
25
+ }
26
+ else {
27
+ headers['Authorization'] = `Bearer ${creds.apiKey}`;
28
+ }
29
+ return headers;
30
+ }
31
+ async function dopomogaiRequest(ctx, opts) {
32
+ const creds = getCreds(ctx);
33
+ const url = new URL(`${creds.apiBaseUrl}${opts.path}`);
34
+ if (opts.query) {
35
+ for (const [k, v] of Object.entries(opts.query)) {
36
+ if (v === undefined || v === null || v === '')
37
+ continue;
38
+ url.searchParams.set(k, String(v));
39
+ }
40
+ }
41
+ const headers = buildHeaders(creds, opts.headers);
42
+ const init = {
43
+ method: opts.method,
44
+ headers,
45
+ };
46
+ if (opts.body !== undefined) {
47
+ headers['Content-Type'] = headers['Content-Type'] || 'application/json';
48
+ init.body = JSON.stringify(opts.body);
49
+ }
50
+ const res = await fetch(url.toString(), init);
51
+ let payload = null;
52
+ const contentType = res.headers.get('content-type') || '';
53
+ if (contentType.includes('application/json')) {
54
+ payload = await res.json().catch(() => null);
55
+ }
56
+ else {
57
+ payload = await res.text().catch(() => null);
58
+ }
59
+ if (!res.ok) {
60
+ const msg = (payload && typeof payload === 'object' && (payload.detail || payload.message)) ||
61
+ (typeof payload === 'string' ? payload : '') ||
62
+ `HTTP ${res.status}`;
63
+ throw new Error(`DopomogAI API error (${res.status}): ${msg}`);
64
+ }
65
+ return { data: payload, status: res.status, headers: res.headers };
66
+ }
@@ -0,0 +1,18 @@
1
+ export type DopomogaiSseEvent = Record<string, any>;
2
+ export declare function postSseAndCollect(opts: {
3
+ url: string;
4
+ headers: Record<string, string>;
5
+ body: any;
6
+ stopOn?: {
7
+ streamEnd?: boolean;
8
+ complete?: boolean;
9
+ };
10
+ maxEvents?: number;
11
+ timeoutMs?: number;
12
+ }): Promise<{
13
+ conversationId?: string | null;
14
+ finalResponse?: string | null;
15
+ accumulatedText: string;
16
+ events: DopomogaiSseEvent[];
17
+ usage?: any;
18
+ }>;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.postSseAndCollect = postSseAndCollect;
4
+ function splitSseBlocks(buffer) {
5
+ // SSE events are separated by a blank line
6
+ const parts = buffer.split(/\r\n\r\n|\r\r|\n\n/);
7
+ const rest = parts.pop() ?? '';
8
+ return { blocks: parts, rest };
9
+ }
10
+ function parseSseDataBlock(block) {
11
+ const lines = block.split(/\r\n|\r|\n/);
12
+ let data = '';
13
+ for (const line of lines) {
14
+ if (line.startsWith('data:'))
15
+ data += line.replace(/^data:\s?/, '');
16
+ }
17
+ return data.trim() ? data : null;
18
+ }
19
+ async function postSseAndCollect(opts) {
20
+ const controller = new AbortController();
21
+ const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1000;
22
+ const t = setTimeout(() => controller.abort(), timeoutMs);
23
+ const res = await fetch(opts.url, {
24
+ method: 'POST',
25
+ headers: {
26
+ Accept: 'text/event-stream',
27
+ 'Content-Type': 'application/json',
28
+ ...opts.headers,
29
+ },
30
+ body: JSON.stringify(opts.body),
31
+ signal: controller.signal,
32
+ });
33
+ if (!res.ok) {
34
+ const text = await res.text().catch(() => '');
35
+ throw new Error(`SSE request failed (${res.status}): ${text}`);
36
+ }
37
+ const conversationId = res.headers.get('x-conversation-id') || res.headers.get('X-Conversation-Id');
38
+ if (!res.body)
39
+ throw new Error('SSE response has no body');
40
+ const reader = res.body.getReader();
41
+ const decoder = new TextDecoder('utf-8');
42
+ let buffer = '';
43
+ let accumulatedText = '';
44
+ let finalResponse = null;
45
+ let usage = undefined;
46
+ const events = [];
47
+ const maxEvents = opts.maxEvents ?? 10_000;
48
+ const stopOnStreamEnd = opts.stopOn?.streamEnd ?? true;
49
+ const stopOnComplete = opts.stopOn?.complete ?? false;
50
+ try {
51
+ while (true) {
52
+ const { done, value } = await reader.read();
53
+ if (done)
54
+ break;
55
+ buffer += decoder.decode(value, { stream: true });
56
+ const { blocks, rest } = splitSseBlocks(buffer);
57
+ buffer = rest;
58
+ for (const block of blocks) {
59
+ const dataStr = parseSseDataBlock(block);
60
+ if (!dataStr)
61
+ continue;
62
+ let evt;
63
+ try {
64
+ evt = JSON.parse(dataStr);
65
+ }
66
+ catch {
67
+ continue;
68
+ }
69
+ events.push(evt);
70
+ if (events.length > maxEvents)
71
+ throw new Error(`Too many SSE events (> ${maxEvents})`);
72
+ if (evt?.type === 'response_chunk' && typeof evt.content === 'string') {
73
+ accumulatedText += evt.content;
74
+ }
75
+ if (evt?.type === 'complete' && typeof evt.final_response === 'string') {
76
+ finalResponse = evt.final_response;
77
+ if (stopOnComplete) {
78
+ controller.abort();
79
+ break;
80
+ }
81
+ }
82
+ if (evt?.type === 'stream_end') {
83
+ // some backends include usage here
84
+ if (evt?.usage)
85
+ usage = evt.usage;
86
+ if (stopOnStreamEnd) {
87
+ controller.abort();
88
+ break;
89
+ }
90
+ }
91
+ if (evt?.type === 'error') {
92
+ const msg = evt?.message || 'Unknown streaming error';
93
+ throw new Error(`Streaming error: ${msg}`);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ finally {
99
+ clearTimeout(t);
100
+ try {
101
+ controller.abort();
102
+ }
103
+ catch { }
104
+ }
105
+ return {
106
+ conversationId,
107
+ finalResponse,
108
+ accumulatedText,
109
+ events,
110
+ usage,
111
+ };
112
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "n8n-nodes-dopomogai",
3
+ "version": "0.1.0",
4
+ "description": "n8n community nodes for DopomogAI (agents, conversations, chat, files).",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": ["dist"],
9
+ "scripts": {
10
+ "build": "rimraf dist && tsc -p tsconfig.json",
11
+ "dev": "tsc -w -p tsconfig.json"
12
+ },
13
+ "dependencies": {
14
+ "n8n-workflow": "^2.2.3",
15
+ "n8n-core": "^2.2.3"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^20.11.30",
19
+ "rimraf": "^6.0.1",
20
+ "typescript": "^5.5.4"
21
+ },
22
+ "keywords": ["n8n-community-node-package", "n8n"],
23
+ "n8n": {
24
+ "n8nNodesApiVersion": 1,
25
+ "credentials": ["dist/credentials/DopomogaiApiKey.credentials.js"],
26
+ "nodes": ["dist/nodes/Dopomogai/Dopomogai.node.js"]
27
+ }
28
+ }