Merge pull request 'Add AI log analysis and history tracking for lines' (#11) from that into main

Reviewed-on: #11
This commit is contained in:
andrew.ng 2025-11-26 20:31:26 +11:00
commit df25b5db3f
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>
<ScrollArea
h={"150px"}
style={{
border: "1px solid #ccc",
borderRadius: "8px",
padding: "4px",
}}
>
{line?.latestScenario?.detectAI ? (
<Box>
<fieldset>
{/* <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;
}