From e0725cc00f09993268703426c1535d8fbb181da4 Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Wed, 26 Nov 2025 16:30:53 +0700 Subject: [PATCH] Add AI log analysis and history tracking for lines Introduces AI-based log analysis to summarize issues and status in network device logs, storing results in the latestScenario.detectAI field. Implements history tracking for line inventory changes using Redis, with backend and socket.io support for retrieving and displaying history. Updates frontend components to show AI analysis results and improve control button states. --- BACKEND/app/services/line_connection.ts | 148 +++++++++++++++++++++- BACKEND/providers/socket_io_provider.ts | 60 ++++++++- FRONTEND/src/components/DragTabs.tsx | 13 ++ FRONTEND/src/components/ModalTerminal.tsx | 67 ++++++++-- FRONTEND/src/untils/types.ts | 15 +++ 5 files changed, 287 insertions(+), 16 deletions(-) diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index 8be4ea5..ef413e7 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs' import { textfsmResults } from './../ultils/templates/index.js' import net from 'node:net' import { @@ -10,6 +11,9 @@ import { import Scenario from '#models/scenario' import Station from '#models/station' import APCController from './apc_connection.js' +import path from 'node:path' +import axios from 'axios' +import redis from '@adonisjs/redis/services/main' interface LineConfig { id: number @@ -31,6 +35,10 @@ interface LineConfig { latestScenario?: { name: string time: number + detectAI?: { + status: string[] + issue: string[] + } } data: { command: string @@ -49,6 +57,17 @@ interface LineConfig { * Scenario */ +interface HistoryItem { + pid: string + vid: string + sn: string + scenario: string + id: number + number: number + stationId: number + timestamp?: number +} + interface User { userEmail: string userName: string @@ -205,8 +224,11 @@ export default class LineConnection { async writeCommand(cmd: string | Buffer, userName = '') { if (this.client.destroyed) { console.log(`⚠️ Cannot send, line ${this.config.lineNumber} is closed`) + this.disconnect() + await sleep(2000) + await this.connect() + // await this.writeCommand(cmd) // if (this.retryConnect <= 3) { - // await sleep(2000) // console.log('Retry connect times', this.retryConnect) // this.retryConnect += 1 // await this.connect() @@ -266,9 +288,9 @@ export default class LineConnection { this.isRunningScript = true const now = Date.now() - this.outputScenario += `\n\n---start-scenarios---${now}---${userName}---\n---scenario---${script?.title}---${now}---\n` + this.outputScenario += `\n\n---start-scenarios---${now}---${userName}---${script?.title}---\n---scenario---${script?.title}---${now}---\n` appendLog( - `\n\n---start-scenarios---${now}---${userName}---\n---scenario---${script?.title}---${now}---\n`, + `\n\n---start-scenarios---${now}---${userName}---${script?.title}---\n---scenario---${script?.title}---${now}---\n`, this.config.stationId, this.config.stationName, this.config.stationIp, @@ -331,11 +353,25 @@ export default class LineConnection { if ( ['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command) ) { - this.config.inventory = JSON.parse(item.textfsm)[0] + const dataInventory = JSON.parse(item.textfsm)[0] + this.config.inventory = dataInventory + this.addHistory(this.config.stationId, this.config.id, { + id: this.config.id, + number: this.config.lineNumber, + stationId: this.config.stationId, + pid: dataInventory?.pid, + sn: dataInventory?.sn, + vid: dataInventory?.vid, + scenario: script?.title, + timestamp: Date.now(), + }) } item.textfsm = JSON.parse(item.textfsm) } }) + const detectLog = await this.detectLogWithAI(logScenarios) + if (this.config.latestScenario) + this.config.latestScenario = { ...this.config.latestScenario, detectAI: detectLog } this.config.data = data this.socketIO.emit('data_textfsm', { stationId: this.config.stationId, @@ -583,4 +619,108 @@ export default class LineConnection { this.writeCommand('write memory\r\n') this.writeCommand('\r\n') } + + async getLog(date: string) { + const logDir = path.join('storage', 'system_logs') + const logFile = path + .join( + logDir, + `${date}-AUTO-Session.${this.config.stationName}-${this.config.stationId}-${this.config.stationIp}-${this.config.lineNumber}.log` + ) + .replaceAll(' ', '_') + + if (!fs.existsSync(logDir) || !fs.existsSync(logFile)) { + return '' + } + + return await fs.promises.readFile(logFile, 'utf8') + } + + async detectLogWithAI(log: string) { + try { + const payload = { + model: 'gpt-4o-mini', + max_tokens: 1000, + messages: [ + { + role: 'user', + content: `Bạn là chuyên gia phân tích log thiết bị mạng. + Hãy phân tích đoạn log sau và xuất kết quả theo đúng format: + + ${log} + + Yêu cầu đầu ra đúng cấu trúc: + + status: + (Tóm tắt trạng thái tổng thể của hệ thống trong 2–4 ý) + + issue: + (Tóm tắt cực ngắn gọn các lỗi/dấu hiệu bất thường, mỗi vấn đề 1 dòng) + + Quy tắc: + Không giải thích dài dòng. + Chỉ tập trung vào lỗi, cảnh báo, thay đổi trạng thái up/down bất thường. + Nếu log không có lỗi → ghi rõ “Không phát hiện vấn đề”. + + Ngắn gọn, dễ đọc, đúng format + Return only json format with English`, + }, + ], + } + const remoteUrl = process.env.ERP_URL || 'https://stage.nswteam.net' + const remoteResp = await axios.post( + remoteUrl + '/api/transferPostData', + { + urlAPI: '/api/open-ai-sfp/model-image-info', + data: payload, + }, + { + headers: { + Authorization: 'Bearer ' + process.env.ERP_TOKEN, + }, + } + ) + return remoteResp.data?.Status === 'OK' ? remoteResp.data?.data : '' + } catch (error: any) { + console.log('[ERROR] Detect log from AI', error) + } + return '' + } + + async addHistory(stationId: number, lineId: number, item: HistoryItem) { + const key = `station:${stationId}:line:${lineId}:history` + const now = Date.now() + + const newItem = JSON.stringify({ + ...item, + timestamp: now, + }) + + console.log(newItem) + + // Lấy phần tử cuối + const lastItems = await redis.zrevrange(key, 0, 0) + if (lastItems.length > 0) { + const last = JSON.parse(lastItems[0]) + + if (last.pid === item.pid && last.sn === item.sn) { + return false // không thay đổi + } + } + + // Add vào ZSET + await redis.zadd(key, now, newItem) + + // Tự động xóa item > 72h + const expireTime = now - 72 * 60 * 60 * 1000 + await redis.zremrangebyscore(key, 0, expireTime) + + return true + } + + async getHistory(stationId: number, lineId: number) { + const key = `station:${stationId}:line:${lineId}:history` + const items = await redis.zrange(key, 0, -1) + return items.map((i) => JSON.parse(i)) + } } diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index e46372b..77f09fe 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -21,6 +21,17 @@ interface HandleOptions { baud?: number } +interface HistoryItem { + pid: string + vid: string + sn: string + scenario: string + id: number + number: number + stationId: number + timestamp?: number +} + type LineAction = (line: LineConnection, options?: HandleOptions) => Promise | void export default class SocketIoProvider { @@ -253,11 +264,14 @@ export class WebSocketIo { io.emit('confirm_take_over', data) }) - socket.on('get_list_logs', async () => { + socket.on('get_list_logs', async (data) => { let getListSystemLogs = fs .readdirSync('storage/system_logs') .map((f) => 'storage/system_logs/' + f) io.to(socket.id).emit('list_logs', getListSystemLogs) + + const listHistory = await this.getHistory(data?.stationId, data?.lineId) + io.to(socket.id).emit('list_histories', listHistory) }) socket.on('get_content_log', async (data) => { @@ -479,6 +493,44 @@ export class WebSocketIo { const { stationId, lineClear } = data await this.clearLineBeforeConnect(stationId, lineClear) }) + + socket.on('get_list_history', async (data) => { + const { stationIds = [] } = data + + if (!Array.isArray(stationIds) || stationIds.length === 0) { + return io.to(socket.id).emit('list_histories', []) + } + + // Xử lý song song tất cả stationId + const stationPromises = stationIds.map(async (stationId) => { + // Lấy station + preload lines + const station = await Station.query().where('id', stationId).preload('lines').first() + + if (!station) { + return { + stationId, + stationName: null, + history: [], + } + } + + // Lấy history cho mỗi line + const linePromises = station.lines.map((line) => this.getHistory(station.id, line.id)) + + const list = await Promise.all(linePromises) + const mergedHistory = list.flat() + + return { + stationId: station.id, + stationName: station.name, + history: mergedHistory, + } + }) + + const result = await Promise.all(stationPromises) + + io.to(socket.id).emit('list_histories', result) + }) }) socketServer.listen(SOCKET_IO_PORT, () => { @@ -942,4 +994,10 @@ export class WebSocketIo { } return result } + + private async getHistory(stationId: number, lineId: number): Promise { + const key = `station:${stationId}:line:${lineId}:history` + const items = await redis.zrange(key, 0, -1) + return items.map((i) => JSON.parse(i)) + } } diff --git a/FRONTEND/src/components/DragTabs.tsx b/FRONTEND/src/components/DragTabs.tsx index 37855f3..7ec1470 100644 --- a/FRONTEND/src/components/DragTabs.tsx +++ b/FRONTEND/src/components/DragTabs.tsx @@ -2,6 +2,7 @@ import { ActionIcon, Avatar, Box, + Button, Flex, Group, Menu, @@ -280,6 +281,18 @@ export default function DraggableTabs({ + {/* */} ( diff --git a/FRONTEND/src/components/ModalTerminal.tsx b/FRONTEND/src/components/ModalTerminal.tsx index 5f40b11..4109e60 100644 --- a/FRONTEND/src/components/ModalTerminal.tsx +++ b/FRONTEND/src/components/ModalTerminal.tsx @@ -422,7 +422,7 @@ const ModalTerminal = ({ } > - + @@ -490,14 +490,59 @@ const ModalTerminal = ({ {""} - + Warning from test report: AI - {""} - - -
+ + {line?.latestScenario?.detectAI ? ( + + {/* + + Status: + + {line?.latestScenario?.detectAI?.status?.map( + (el, i) => ( + + - {el} + + ) + )} + */} + + + Issue: + + {line?.latestScenario?.detectAI?.issue?.map((el, i) => ( + + - {el} + + ))} + + + ) : ( + "" + )} + + + +
- Not Connected + Not config )} @@ -531,7 +576,7 @@ const ModalTerminal = ({