feat: version 4.14.0 -- Equipos: año del procesador con boton Obtener fecha (IA)

La IA consulta solo el nombre del procesador y devuelve el año (el mas antiguo si hay dos).
Migracion 039.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-29 19:07:31 -05:00
parent 0968e61ced
commit b2c25c4fe5
14 changed files with 89 additions and 6 deletions
@@ -23,3 +23,5 @@ export const getAccess = asyncHandler(async (req, res) => ok(res, await service.
export const getViewers = asyncHandler(async (req, res) => ok(res, await service.listViewers()));
export const postViewer = asyncHandler(async (req, res) => ok(res, await service.addViewer(Number(req.body.userId), req.currentUser), 201));
export const deleteViewer = asyncHandler(async (req, res) => ok(res, await service.removeViewer(Number(req.params.userId), req.currentUser)));
export const postCpuYear = asyncHandler(async (req, res) => ok(res, await service.fetchCpuYear(Number(req.params.id), req.currentUser)));
@@ -214,6 +214,8 @@ export async function findById(id) {
[id]
);
detail.changes = changes;
// El año del procesador solo vale mientras el equipo siga reportando el mismo procesador.
if (detail.cpu_year_for !== detail.cpu_summary) detail.cpu_year = null;
return detail;
}
@@ -225,6 +227,8 @@ export async function updateManual(id, { status, assignedUserId, departmentId, n
return result.affectedRows > 0;
}
export const saveCpuYear = (id, year, cpuSummary) => pool.query('UPDATE devices SET cpu_year = ?, cpu_year_for = ? WHERE id = ?', [year, cpuSummary, id]);
export async function findRawFile(id) {
const [[row]] = await pool.query('SELECT raw_file FROM devices WHERE id = ?', [id]);
return row?.raw_file || null;
+6 -1
View File
@@ -1,15 +1,19 @@
import { Router } from 'express';
import rateLimit from 'express-rate-limit';
import { requireAuth, requireRole, CAN_EDIT_DEVICES, CAN_MANAGE_DEVICES } from '../../middleware/auth.js';
import { requireModule } from '../../middleware/modules.js';
import { fail } from '../../utils/http.js';
import { getAccess as getAccessFor } from './service.js';
import {
getDevices, getDevice, putDevice, getDashboard, getSoftware, getSoftwareDevices, getRaw,
getTokens, postToken, patchToken, deleteToken, getTokenSecret, getAccess, getViewers, postViewer, deleteViewer,
getTokens, postToken, patchToken, deleteToken, getTokenSecret, getAccess, getViewers, postViewer, deleteViewer, postCpuYear,
} from './controller.js';
const router = Router();
// Cada consulta a la IA cuesta: tope por IP para no gastar saldo por error o abuso.
const aiLimiter = rateLimit({ windowMs: 60 * 60 * 1000, limit: 30, standardHeaders: true, legacyHeaders: false });
// Inventario: lo ve y lo edita quien gestiona TI. Los tokens del agente y el compartir, solo
// admin/superadmin. Otros usuarios pueden tener acceso de SOLO LECTURA compartido (device_viewers).
@@ -38,6 +42,7 @@ router.delete('/tokens/:id', requireRole(...CAN_MANAGE_DEVICES), deleteToken);
router.get('/', canView, getDevices);
router.get('/:id', canView, getDevice);
router.get('/:id/raw', requireRole(...CAN_EDIT_DEVICES), getRaw);
router.post('/:id/cpu-year', requireRole(...CAN_EDIT_DEVICES), aiLimiter, postCpuYear);
router.put('/:id', requireRole(...CAN_EDIT_DEVICES), putDevice);
export default router;
+25
View File
@@ -6,6 +6,7 @@ import { HttpError } from '../../utils/http.js';
import { requireEditVersion, requireUpdated } from '../../utils/editVersion.js';
import { logAudit } from '../../utils/audit.js';
import { encrypt, decrypt } from '../../utils/crypto.js';
import { askStructured } from '../../utils/ai.js';
import * as repo from './repository.js';
import { CAN_EDIT_DEVICES, CAN_MANAGE_DEVICES } from '../../middleware/auth.js';
@@ -42,6 +43,30 @@ export async function updateDevice(id, data, actor) {
return getDevice(id);
}
// Año de lanzamiento del procesador, consultado a la IA SOLO con el nombre del procesador (nada
// del equipo, usuario ni red). Si la IA da más de una fecha se pide la más antigua. La IA solo
// sugiere: se valida el rango antes de guardar.
export async function fetchCpuYear(id, actor) {
const device = await getDevice(id);
const cpuName = device.cpus.find((c) => c.name)?.name;
if (!cpuName) throw new HttpError(400, 'Este equipo no reporta procesador');
const result = await askStructured({
system: 'Eres un experto en hardware de computadores. Te dan SOLO el nombre de un procesador. Indica el AÑO en que se lanzó. Si hay más de una fecha asociada (por ejemplo anuncio y disponibilidad), devuelve el año MÁS ANTIGUO. Si no puedes determinarlo con seguridad, devuelve null: no inventes.',
prompt: `Procesador: ${String(cpuName).trim().slice(0, 200)}`,
toolName: 'cpu_release_year',
schema: { type: 'object', properties: { year: { type: ['integer', 'null'], description: 'Año de lanzamiento (4 dígitos) o null' } }, required: ['year'] },
maxTokens: 200,
usage: { userId: actor.id, action: 'device_cpu_year' },
});
const year = Number(result?.year);
if (!Number.isInteger(year) || year < 1990 || year > new Date().getFullYear() + 1) {
throw new HttpError(422, 'No se pudo determinar el año de este procesador');
}
await repo.saveCpuYear(id, year, device.cpu_summary);
await logAudit({ userId: actor.id, action: 'cpu_year', entityType: 'device', entityId: id, detail: { cpu: cpuName, year } });
return getDevice(id);
}
export async function getRaw(id) {
const file = await repo.findRawFile(id);
if (!file) throw new HttpError(404, 'Sin inventario crudo para este equipo');