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:
parent
6f785644be
commit
e0725cc00f
|
|
@ -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 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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue