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.
This commit is contained in:
nguyentrungthat 2025-11-26 16:30:53 +07:00
parent 6f785644be
commit e0725cc00f
5 changed files with 287 additions and 16 deletions

View File

@ -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<ArrayBuffer>, 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 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 24 ý)
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 lỗi ghi 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))
}
}

View File

@ -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 | unknown> | 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<HistoryItem[]> {
const key = `station:${stationId}:line:${lineId}:history`
const items = await redis.zrange(key, 0, -1)
return items.map((i) => JSON.parse(i))
}
}

View File

@ -2,6 +2,7 @@ import {
ActionIcon,
Avatar,
Box,
Button,
Flex,
Group,
Menu,
@ -280,6 +281,18 @@ export default function DraggableTabs({
</Flex>
</Tabs.List>
<Flex align={"center"}>
{/* <Button
variant="outline"
style={{ height: "26px", width: "80px", marginRight: "20px" }}
size="xs"
onClick={() => {
socket?.emit("get_list_history", {
stationIds: tabs?.map((el) => el.id),
});
}}
>
History
</Button> */}
<Tooltip
withArrow
label={usersConnecting.map((el) => (

View File

@ -422,7 +422,7 @@ const ModalTerminal = ({
}
>
<Grid>
<Grid.Col span={2}>
<Grid.Col span={3}>
<Flex justify={"space-between"} direction={"column"} h={"95%"}>
<Box>
<Flex gap={"sm"} justify={"center"} align={"center"}>
@ -490,14 +490,59 @@ const ModalTerminal = ({
</Text>
<Text size="md">{""}</Text>
</Flex>
<Flex>
<Box>
<Text size="md" mr={"sm"} fw={"bold"}>
Warning from test report: AI
</Text>
<Text size="md">{""}</Text>
</Flex>
<Box>
<fieldset>
<ScrollArea
h={"150px"}
style={{
border: "1px solid #ccc",
borderRadius: "8px",
padding: "4px",
}}
>
{line?.latestScenario?.detectAI ? (
<Box>
{/* <Box>
<Text size="sm" fw={"bold"}>
Status:
</Text>
{line?.latestScenario?.detectAI?.status?.map(
(el, i) => (
<Box key={i}>
<Text size="sm">- {el}</Text>
</Box>
)
)}
</Box> */}
<Box>
<Text size="sm" fw={"bold"}>
Issue:
</Text>
{line?.latestScenario?.detectAI?.issue?.map((el, i) => (
<Box key={i}>
<Text size="sm">- {el}</Text>
</Box>
))}
</Box>
</Box>
) : (
""
)}
</ScrollArea>
</Box>
<Box
style={{
display: "flex",
justifyContent: "center",
}}
>
<fieldset
style={{
width: "280px",
}}
>
<Flex justify={"center"}>
<IconCircleDot
color={
@ -523,7 +568,7 @@ const ModalTerminal = ({
)
) : (
<Text c={"red"} size="sm" ml={"4px"}>
Not Connected
Not config
</Text>
)}
</Flex>
@ -531,7 +576,7 @@ const ModalTerminal = ({
<Flex justify={"space-around"} mt={"4px"}>
<Button
className={classes.buttonControl}
disabled={isDisable}
disabled={isDisable || !line?.interface}
fw={400}
variant="outline"
color="green"
@ -544,7 +589,7 @@ const ModalTerminal = ({
</Button>
<Button
className={classes.buttonControl}
disabled={isDisable}
disabled={isDisable || !line?.interface}
fw={400}
variant="outline"
color="red"
@ -557,7 +602,7 @@ const ModalTerminal = ({
</Button>
<Button
className={classes.buttonControl}
disabled={isDisable}
disabled={isDisable || !line?.interface}
fw={400}
variant="outline"
color="orange"
@ -612,7 +657,7 @@ const ModalTerminal = ({
</Flex>
</Grid.Col>
<Grid.Col
span={10}
span={9}
style={{
// borderRight: "1px solid #ccc",
borderLeft: "1px solid #ccc",

View File

@ -91,6 +91,10 @@ export type TLine = {
latestScenario?: {
name: string;
time: number;
detectAI?: {
status: string[];
issue: string[];
};
};
commands?: string[];
interface?: string;
@ -219,3 +223,14 @@ export type THistoryTicket = {
userName: string;
userId: number;
};
interface HistoryItem {
pid: string;
vid: string;
sn: string;
scenario: string;
id: number;
number: number;
stationId: number;
timestamp?: number;
}