336 lines
8.8 KiB
TypeScript
336 lines
8.8 KiB
TypeScript
import Scenario from '#models/scenario'
|
||
import fs from 'node:fs'
|
||
import path from 'node:path'
|
||
import nodeMailer from 'nodemailer'
|
||
import zulip from 'zulip-js'
|
||
|
||
type DetectAI = {
|
||
status: string[]
|
||
issue: string[]
|
||
summary: string
|
||
}
|
||
|
||
type InputData = {
|
||
lineNumber: number
|
||
inventory: any
|
||
latestScenario?: {
|
||
detectAI?: DetectAI
|
||
}
|
||
data?: any[]
|
||
}
|
||
|
||
// Types
|
||
type SendMailResponse = string
|
||
type SendMessageType = 'stream' | 'private'
|
||
|
||
/**
|
||
* Function to clean up unwanted characters from the output data.
|
||
* @param {string} data - The raw data to be cleaned.
|
||
* @returns {string} - The cleaned data.
|
||
*/
|
||
export const cleanData = (data: string) => {
|
||
return (
|
||
data
|
||
// 1️⃣ Xóa chuỗi "--More--" (Cisco/Unix pager)
|
||
.replace(/--More--[\s\x08\x1b\[K]*/g, '')
|
||
|
||
// 2️⃣ Xóa toàn bộ chuỗi ANSI escape sequences
|
||
// Ví dụ: ESC[2J, ESC[K, ESC[?25h, ESC[0m, ...
|
||
.replace(/\x1B\[[0-9;?]*[A-Za-z]/g, '')
|
||
|
||
// 3️⃣ Xóa ký tự Backspace (BS) hoặc Delete (DEL)
|
||
.replace(/[\x08\x7F]/g, '')
|
||
|
||
// 4️⃣ Xóa ký tự NUL và các control char khác (trừ \r, \n, \t)
|
||
.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, '')
|
||
)
|
||
|
||
// 5️⃣ Chuẩn hóa xuống dòng nếu cần
|
||
// .replace(/\r\n/g, '\n')
|
||
}
|
||
|
||
export function sleep(ms: number) {
|
||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||
}
|
||
|
||
// 20250527-AUTO-Session.Station_1-13-192.168.171.9-2.log
|
||
// {DATE}-AUTO-Session.{Station name}-{Station ID}-{Station IP}-{Line number}.log
|
||
export function appendLog(
|
||
output: string,
|
||
stationId: number,
|
||
stationName: string,
|
||
stationIP: string,
|
||
lineNumber: number | string
|
||
) {
|
||
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '') // YYYYMMDD
|
||
const logDir = path.join('storage', 'system_logs')
|
||
const logFile = path
|
||
.join(logDir, `${date}-AUTO-Session.${stationName}-${stationId}-${stationIP}-${lineNumber}.log`)
|
||
.replaceAll(' ', '_')
|
||
|
||
// Ensure folder exists
|
||
if (!fs.existsSync(logDir)) {
|
||
fs.mkdirSync(logDir, { recursive: true })
|
||
}
|
||
|
||
fs.appendFile(logFile, output, (err) => {
|
||
if (err) {
|
||
console.error('❌ Failed to write log:', err.message)
|
||
}
|
||
})
|
||
}
|
||
|
||
export const getPathLog = (stationId: number, lineNumber: number, port: number) => {
|
||
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '') // YYYYMMDD
|
||
const logDir = path.join('storage', 'system_logs')
|
||
const logFile = path.join(logDir, `${date}-Station_${stationId}-Line_${lineNumber}_${port}.log`)
|
||
// Ensure folder exists
|
||
if (!fs.existsSync(logDir)) {
|
||
fs.mkdirSync(logDir, { recursive: true })
|
||
return null
|
||
} else return logFile
|
||
}
|
||
|
||
/**
|
||
* Utility function get scope log with timestamp.
|
||
* @param {string} text - content log.
|
||
* @param {number} time - Timestamp.
|
||
*/
|
||
export const getLogWithTimeScenario = (text: string, time: number) => {
|
||
try {
|
||
// Match all start and end blocks
|
||
const regex = /---(start|end)-scenarios---(\d+)---/g
|
||
|
||
let match
|
||
const blocks = []
|
||
|
||
while ((match = regex.exec(text)) !== null) {
|
||
blocks.push({
|
||
type: match[1],
|
||
timestamp: match[2],
|
||
index: match.index,
|
||
})
|
||
}
|
||
|
||
// Find the matching block for the end timestamp
|
||
let result = null
|
||
for (let i = 0; i < blocks.length; i++) {
|
||
const block = blocks[i]
|
||
if (block.type === 'end' && block.timestamp === time.toString()) {
|
||
// Find nearest preceding "start"
|
||
for (let j = i - 1; j >= 0; j--) {
|
||
if (blocks[j].type === 'start') {
|
||
const startIndex = blocks[j].index
|
||
const endIndex = block.index + text.slice(block.index).indexOf('\n') // or manually offset length of the line
|
||
result = text.slice(startIndex, endIndex).trim()
|
||
break
|
||
}
|
||
}
|
||
break
|
||
}
|
||
}
|
||
return result
|
||
} catch (err) {
|
||
console.error('Error get log:', err)
|
||
return ''
|
||
}
|
||
}
|
||
|
||
export function isValidJson(string: string) {
|
||
try {
|
||
JSON.parse(string)
|
||
return true // Chuỗi là định dạng JSON hợp lệ
|
||
} catch (e) {
|
||
return false // Chuỗi không phải là định dạng JSON hợp lệ
|
||
}
|
||
}
|
||
|
||
export function mapToLineFormat(input: InputData) {
|
||
const line = input.lineNumber
|
||
|
||
const pid = input.inventory?.pid || ''
|
||
const vid = input.inventory?.vid || ''
|
||
const sn = input.inventory?.sn || ''
|
||
|
||
if (!pid || !sn) {
|
||
return {
|
||
line,
|
||
pid: '',
|
||
vid: '',
|
||
sn: '',
|
||
ios: '',
|
||
mac: '',
|
||
license: [],
|
||
issues: ['No data'],
|
||
summary: '',
|
||
}
|
||
}
|
||
|
||
// MAC
|
||
let mac = ''
|
||
let ios = ''
|
||
const showVersion = input.data?.find(
|
||
(d) =>
|
||
d.command === 'show version' ||
|
||
d.command === 'sh version' ||
|
||
d.command === 'show ver' ||
|
||
d.command === 'sh ver'
|
||
)
|
||
if (showVersion?.textfsm?.[0]?.MAC_ADDRESS) {
|
||
mac = showVersion.textfsm[0].MAC_ADDRESS
|
||
}
|
||
if (showVersion?.textfsm?.[0]?.SOFTWARE_IMAGE) {
|
||
ios = showVersion.textfsm[0].SOFTWARE_IMAGE + ' ' + (showVersion?.textfsm?.[0]?.VERSION || '')
|
||
}
|
||
|
||
// License
|
||
const dataLicense = input.data?.find((comm) => comm.command?.trim() === 'show license')
|
||
const license =
|
||
dataLicense?.textfsm && Array.isArray(dataLicense.textfsm)
|
||
? dataLicense.textfsm
|
||
?.filter((el: any) => el.LICENSE_TYPE === 'Permanent')
|
||
.map((v: any) => v.FEATURE)
|
||
: ''
|
||
|
||
// // Mode (DPEL / DPELP)
|
||
// const dataPlatform = input.data?.find((el) => el.command?.trim() === 'show platform')
|
||
// const mode = dataPlatform && !dataPlatform.output?.includes('Incomplete') ? 'DPELP' : 'DPEL'
|
||
|
||
// Issues
|
||
const issues = Array.isArray(input.latestScenario?.detectAI?.issue)
|
||
? input.latestScenario.detectAI.issue
|
||
: input.latestScenario?.detectAI?.issue
|
||
? [input.latestScenario.detectAI.issue]
|
||
: []
|
||
// Issues
|
||
const summary = input.latestScenario?.detectAI?.summary || ''
|
||
|
||
return {
|
||
line,
|
||
pid,
|
||
vid,
|
||
sn,
|
||
ios,
|
||
mac,
|
||
license,
|
||
issues,
|
||
summary,
|
||
}
|
||
}
|
||
|
||
export function sendMessageToMail(
|
||
email: string,
|
||
subject: string,
|
||
text: string,
|
||
cc?: string[]
|
||
): Promise<SendMailResponse> {
|
||
return new Promise((resolve, reject) => {
|
||
const transporter = nodeMailer.createTransport({
|
||
pool: true,
|
||
host: process.env.SMTP_HOST,
|
||
port: Number(process.env.SMTP_PORT),
|
||
secure: true,
|
||
auth: {
|
||
user: process.env.SMTP_USERNAME,
|
||
pass: process.env.SMTP_PASSWORD,
|
||
},
|
||
})
|
||
|
||
const mailOptions = {
|
||
from: process.env.SMTP_USERNAME,
|
||
to: email,
|
||
subject,
|
||
html: text,
|
||
cc: cc,
|
||
}
|
||
|
||
transporter.sendMail(mailOptions, (error: any, info: any) => {
|
||
if (error) {
|
||
console.error(error)
|
||
reject(error)
|
||
} else {
|
||
console.log('Email sent: ' + info.response)
|
||
resolve(info.response)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
export function sendMessageToZulip(
|
||
type: SendMessageType,
|
||
to: string | number | string[],
|
||
topic: string | undefined,
|
||
content: string
|
||
): Promise<any> | null {
|
||
return new Promise((resolve, reject) => {
|
||
const config = {
|
||
realm: process.env.ZULIP_REALM as string,
|
||
username: process.env.ZULIP_USERNAME as string,
|
||
apiKey: process.env.ZULIP_API_KEY as string,
|
||
}
|
||
|
||
zulip(config).then((client: any) => {
|
||
if (type === 'stream') {
|
||
client.messages
|
||
.send({
|
||
type,
|
||
to,
|
||
topic: topic || '',
|
||
content,
|
||
})
|
||
.then((response: any) => {
|
||
console.log('Message sent: ' + JSON.stringify(response))
|
||
resolve(response)
|
||
})
|
||
.catch((error: any) => {
|
||
console.error(error)
|
||
reject(error)
|
||
})
|
||
} else if (type === 'private') {
|
||
client.messages
|
||
.send({
|
||
type,
|
||
to,
|
||
content,
|
||
})
|
||
.then((response: any) => {
|
||
console.log('Message sent: ' + JSON.stringify(response))
|
||
resolve(response)
|
||
})
|
||
.catch((error: any) => {
|
||
console.error(error)
|
||
reject(error)
|
||
})
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
// Catch scenario with key longer
|
||
export const detectScenarioByModel = async (model: string, listScenarios: number[]) => {
|
||
let scenarios = await Scenario.query().preload('brand').preload('category')
|
||
const normalizedModel = model.trim().toUpperCase()
|
||
let matched: { scenario: Scenario; score: number } | null = null
|
||
|
||
for (const scenario of scenarios) {
|
||
if (listScenarios.includes(scenario.id)) continue
|
||
const seriesList: string[] = Array.isArray(scenario.series)
|
||
? scenario.series
|
||
: JSON.parse(scenario.series || '[]')
|
||
|
||
for (const s of seriesList) {
|
||
const pattern = s.trim().toUpperCase()
|
||
|
||
if (normalizedModel.startsWith(pattern)) {
|
||
const score = pattern.length
|
||
|
||
if (!matched || score > matched.score) {
|
||
matched = { scenario, score }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return matched?.scenario || null
|
||
}
|