@alvarobr/n8n-nodes-glpi-connect 1.0.1 → 1.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
CHANGED
|
@@ -28,7 +28,7 @@ Creado por **Álvaro Blanca Rodríguez**.
|
|
|
28
28
|
- **Detalle**: técnicos, grupos, seguimientos y tareas públicos, soluciones, aprobaciones y activos asociados.
|
|
29
29
|
- **Crear**: incidencia o solicitud con el usuario como solicitante y, opcionalmente, su equipo afectado. Antes de crearla avisa de posibles duplicados.
|
|
30
30
|
- **Añadir comentario**: seguimiento firmado con el nombre del usuario. En un ticket resuelto solicita su reapertura.
|
|
31
|
-
- **Cerrar o cancelar**: registra la solución
|
|
31
|
+
- **Cerrar o cancelar**: registra un seguimiento con la confirmación del usuario, asigna el ticket a la cuenta técnica de la credencial (sin quitar a los técnicos anteriores), añade la solución, lo cierra y vuelve a consultarlo para confirmar el estado final. El motivo puede ser «solucionado», «cancelado» o «acción automática completada».
|
|
32
32
|
- **Asociar activo**: relación nativa ticket-elemento de GLPI, solo con activos del usuario y sin duplicados.
|
|
33
33
|
|
|
34
34
|
### Activos
|
|
@@ -23,6 +23,7 @@ class GlpiClient {
|
|
|
23
23
|
this.config = config;
|
|
24
24
|
this.http = http;
|
|
25
25
|
this.sessionToken = null;
|
|
26
|
+
this.sessionUser = null;
|
|
26
27
|
const base = config.url.trim().replace(/\/+$/, '').replace(/\/apirest\.php$/, '');
|
|
27
28
|
this.api = `${base}/apirest.php`;
|
|
28
29
|
this.front = `${base}/front`;
|
|
@@ -59,6 +60,18 @@ class GlpiClient {
|
|
|
59
60
|
// La sesión caducará sola en GLPI
|
|
60
61
|
}
|
|
61
62
|
this.sessionToken = null;
|
|
63
|
+
this.sessionUser = null;
|
|
64
|
+
}
|
|
65
|
+
/** ID del usuario de GLPI con el que se ha iniciado sesión (la cuenta técnica del token). */
|
|
66
|
+
async sessionUserId() {
|
|
67
|
+
if (this.sessionUser)
|
|
68
|
+
return this.sessionUser;
|
|
69
|
+
const body = await this.get('getFullSession');
|
|
70
|
+
const id = Number(body?.session?.glpiID);
|
|
71
|
+
if (!Number.isSafeInteger(id) || id <= 0)
|
|
72
|
+
throw new GlpiError('GLPI no ha indicado el usuario de la sesión.', 200, 'NO_SESSION_USER');
|
|
73
|
+
this.sessionUser = id;
|
|
74
|
+
return id;
|
|
62
75
|
}
|
|
63
76
|
/** Petición que devuelve el estado y el cuerpo sin lanzar error (para decidir según la respuesta). */
|
|
64
77
|
async raw(method, path, options = {}) {
|
|
@@ -80,7 +80,7 @@ const properties = [
|
|
|
80
80
|
{ name: 'Detalle', value: 'detalle', action: 'Ver el detalle de un ticket del usuario', description: 'Técnicos, seguimientos, tareas, soluciones, aprobaciones y activos asociados' },
|
|
81
81
|
{ name: 'Crear', value: 'crear', action: 'Crear un ticket para el usuario', description: 'Crea una incidencia o solicitud, opcionalmente asociada a un activo del usuario' },
|
|
82
82
|
{ name: 'Añadir comentario', value: 'comentar', action: 'Añadir un comentario a un ticket del usuario', description: 'Añade un seguimiento; en un ticket resuelto solicita su reapertura' },
|
|
83
|
-
{ name: 'Cerrar o cancelar', value: 'cerrar', action: 'Cerrar o cancelar un ticket del usuario', description: 'Registra la
|
|
83
|
+
{ name: 'Cerrar o cancelar', value: 'cerrar', action: 'Cerrar o cancelar un ticket del usuario', description: 'Registra un seguimiento, asigna el ticket a la cuenta de la integración, lo cierra y comprueba el estado final' },
|
|
84
84
|
{ name: 'Asociar activo', value: 'asociarActivo', action: 'Asociar un activo del usuario a su ticket', description: 'Crea la relación nativa ticket-elemento de GLPI' },
|
|
85
85
|
],
|
|
86
86
|
default: 'listar',
|
|
@@ -262,6 +262,7 @@ const properties = [
|
|
|
262
262
|
options: [
|
|
263
263
|
{ name: 'Solucionado', value: 'solucionado' },
|
|
264
264
|
{ name: 'Cancelado (ya no se necesita)', value: 'cancelado' },
|
|
265
|
+
{ name: 'Acción automática completada', value: 'automatizado' },
|
|
265
266
|
],
|
|
266
267
|
default: 'solucionado',
|
|
267
268
|
displayOptions: show(['ticket'], ['cerrar']),
|
|
@@ -341,6 +341,28 @@ async function addFollowup(client, user, ticketId, text, control) {
|
|
|
341
341
|
return { ok: false, ejecutado: false, error: 'GLPI no ha aceptado el comentario. No se ha modificado el ticket.', detalle: glpiDetail(error), ticket };
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
|
+
const CLOSE_TEXTS = {
|
|
345
|
+
solucionado: {
|
|
346
|
+
label: 'Cerrar como solucionado',
|
|
347
|
+
followup: 'El usuario ha confirmado la resolución de la incidencia.',
|
|
348
|
+
solution: 'El usuario indica que el problema se ha solucionado.',
|
|
349
|
+
},
|
|
350
|
+
cancelado: {
|
|
351
|
+
label: 'Cancelar',
|
|
352
|
+
followup: 'El usuario ha confirmado que ya no necesita esta solicitud y pide cancelarla.',
|
|
353
|
+
solution: 'Solicitud cancelada por el usuario: ya no la necesita.',
|
|
354
|
+
},
|
|
355
|
+
automatizado: {
|
|
356
|
+
label: 'Cerrar tras la acción automática',
|
|
357
|
+
followup: 'Acción ejecutada automáticamente por el asistente a petición del usuario.',
|
|
358
|
+
solution: 'Resuelto mediante una acción automática del asistente de soporte.',
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
/**
|
|
362
|
+
* Cierre completo: seguimiento, asignación a la cuenta técnica de la integración,
|
|
363
|
+
* solución, estado Cerrado y comprobación final en GLPI.
|
|
364
|
+
* Los técnicos, seguimientos y soluciones anteriores se conservan.
|
|
365
|
+
*/
|
|
344
366
|
async function closeTicket(client, user, ticketId, reason, text, control) {
|
|
345
367
|
const access = await loadEditableTicket(client, user, ticketId, true);
|
|
346
368
|
if (!isAccess(access))
|
|
@@ -348,34 +370,81 @@ async function closeTicket(client, user, ticketId, reason, text, control) {
|
|
|
348
370
|
const ticket = access.summary;
|
|
349
371
|
if (ticket.estado_id === 6)
|
|
350
372
|
return { ok: true, ejecutado: false, mensaje: `El ticket #${ticket.id} ya estaba cerrado.`, ticket };
|
|
373
|
+
const texts = CLOSE_TEXTS[reason] ?? CLOSE_TEXTS.solucionado;
|
|
351
374
|
const explanation = String(text ?? '').trim();
|
|
352
|
-
const
|
|
375
|
+
const extra = explanation ? `<p>${(0, utils_1.escapeHtml)(explanation)}</p>` : '';
|
|
353
376
|
const { execute, warning } = decide(control);
|
|
354
377
|
if (!execute) {
|
|
355
|
-
return preview(`${label} el ticket #${ticket.id} «${ticket.titulo}»${explanation ? ` indicando: "${explanation}"` : ''}`, warning, { ticket });
|
|
378
|
+
return preview(`${texts.label} el ticket #${ticket.id} «${ticket.titulo}»${explanation ? ` indicando: "${explanation}"` : ''}`, warning, { ticket });
|
|
379
|
+
}
|
|
380
|
+
// 1. Seguimiento con la confirmación del usuario
|
|
381
|
+
try {
|
|
382
|
+
await client.call('POST', 'ITILFollowup', {
|
|
383
|
+
body: { input: { itemtype: 'Ticket', items_id: ticket.id, is_private: 0, content: `<p>${texts.followup}</p>${extra}${signature(user)}` } },
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
return { ok: false, ejecutado: false, error: `GLPI no ha permitido registrar el seguimiento del ticket #${ticket.id}. No se ha modificado.`, detalle: glpiDetail(error), ticket };
|
|
388
|
+
}
|
|
389
|
+
// 2. Asignar a la cuenta técnica (se añade; los técnicos anteriores se mantienen)
|
|
390
|
+
const avisos = [];
|
|
391
|
+
try {
|
|
392
|
+
const agentId = await client.sessionUserId();
|
|
393
|
+
const actors = await client.get(`Ticket/${ticket.id}/Ticket_User`).catch(() => []);
|
|
394
|
+
const assigned = (Array.isArray(actors) ? actors : []).some((a) => a && Number(a.users_id) === agentId && Number(a.type) === 2);
|
|
395
|
+
if (!assigned)
|
|
396
|
+
await client.call('POST', 'Ticket_User', { body: { input: { tickets_id: ticket.id, users_id: agentId, type: 2 } } });
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
avisos.push(`No se ha podido asignar el ticket a la cuenta técnica: ${glpiDetail(error)}`);
|
|
356
400
|
}
|
|
357
|
-
//
|
|
401
|
+
// 3. Solución (GLPI la necesita para pasar a Resuelto) y 4. Cerrado
|
|
358
402
|
if (ticket.estado_id !== 5) {
|
|
359
|
-
const heading = reason === 'cancelado' ? 'Solicitud cancelada por el usuario: ya no la necesita.' : 'El usuario indica que el problema se ha solucionado.';
|
|
360
403
|
try {
|
|
361
404
|
await client.call('POST', 'ITILSolution', {
|
|
362
|
-
body: { input: { itemtype: 'Ticket', items_id: ticket.id, content: `<p>${
|
|
405
|
+
body: { input: { itemtype: 'Ticket', items_id: ticket.id, content: `<p>${texts.solution}</p>${extra}${signature(user)}` } },
|
|
363
406
|
});
|
|
364
407
|
}
|
|
365
408
|
catch (error) {
|
|
366
|
-
return
|
|
409
|
+
return (0, utils_1.compact)({
|
|
410
|
+
ok: false,
|
|
411
|
+
ejecutado: 'parcial',
|
|
412
|
+
error: `Se ha registrado el seguimiento, pero GLPI no ha permitido resolver el ticket #${ticket.id}, así que sigue abierto.`,
|
|
413
|
+
detalle: glpiDetail(error),
|
|
414
|
+
avisos: avisos.length ? avisos : undefined,
|
|
415
|
+
ticket,
|
|
416
|
+
});
|
|
367
417
|
}
|
|
368
418
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
419
|
+
await client.raw('PUT', `Ticket/${ticket.id}`, { body: { input: { id: ticket.id, status: 6 } } });
|
|
420
|
+
// 5. Comprobar en GLPI el estado final antes de dar el cierre por hecho
|
|
421
|
+
const check = await client.raw('GET', `Ticket/${ticket.id}`);
|
|
422
|
+
const finalStatus = check.statusCode < 300 && check.body && !Array.isArray(check.body) ? Number(check.body.status) : NaN;
|
|
423
|
+
const final = { ...ticket, estado: utils_1.TICKET_STATUS[finalStatus] ?? 'Desconocido', estado_id: finalStatus };
|
|
424
|
+
const notes = avisos.length ? avisos : undefined;
|
|
425
|
+
if (finalStatus === 6) {
|
|
426
|
+
return (0, utils_1.compact)({ ok: true, ejecutado: true, verificado: true, mensaje: `GLPI confirma que el ticket #${ticket.id} está cerrado.`, avisos: notes, ticket: final });
|
|
427
|
+
}
|
|
428
|
+
if (finalStatus === 5) {
|
|
429
|
+
return (0, utils_1.compact)({
|
|
430
|
+
ok: true,
|
|
431
|
+
ejecutado: 'parcial',
|
|
432
|
+
verificado: true,
|
|
433
|
+
mensaje: `El ticket #${ticket.id} ha quedado como Resuelto, pero GLPI no ha permitido cerrarlo directamente. Se cerrará automáticamente según la configuración de GLPI.`,
|
|
434
|
+
avisos: notes,
|
|
435
|
+
ticket: final,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
return (0, utils_1.compact)({
|
|
439
|
+
ok: false,
|
|
375
440
|
ejecutado: 'parcial',
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
441
|
+
verificado: Number.isFinite(finalStatus),
|
|
442
|
+
error: Number.isFinite(finalStatus)
|
|
443
|
+
? `GLPI indica que el ticket #${ticket.id} sigue en estado «${final.estado}». No se ha cerrado.`
|
|
444
|
+
: `No se ha podido comprobar en GLPI el estado final del ticket #${ticket.id}.`,
|
|
445
|
+
avisos: notes,
|
|
446
|
+
ticket: final,
|
|
447
|
+
});
|
|
379
448
|
}
|
|
380
449
|
/** Comprueba que un activo esté asignado al usuario. */
|
|
381
450
|
async function loadOwnAsset(client, user, itemtype, itemsId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alvarobr/n8n-nodes-glpi-connect",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Nodo de n8n para GLPI: autoservicio IT seguro para flujos y agentes de IA. Tickets, inventario, software y base de conocimiento con sesión automática, permisos por usuario y confirmación de cambios.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|