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:
commit
df25b5db3f
|
|
@ -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