Update send zulip

This commit is contained in:
nguyentrungthat 2025-12-08 16:30:17 +07:00
parent a363883329
commit 4e5099aea8
11 changed files with 333 additions and 25 deletions

View File

@ -22,4 +22,7 @@ export default class Scenario extends BaseModel {
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
@column()
declare send_result: boolean
}

View File

@ -82,4 +82,7 @@ export default class Station extends BaseModel {
@column()
declare send_wiki: boolean
@column()
declare is_active: boolean
}

View File

@ -165,7 +165,7 @@ export default class LineConnection {
if (!this.config.inventory)
this.outputInventory = this.outputInventory.slice(-3000) + message
}
if (message.includes('--More--')) this.writeCommand(' ')
if (data.toString().includes('--More--')) this.writeCommand(' ')
// let output = cleanData(message)
// console.log(`📨 [${this.config.port}] ${message}`)
@ -735,19 +735,23 @@ export default class LineConnection {
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:
Yêu cầu đu ra đúng cấu trúc:
issue:
(Tóm tắt ngắn gọn các lỗi/dấu hiệu bất thường, mỗi vấn đ 1 dòng, bỏ qua các vấn đ không quan trọng như về port up/down hay Invalid input, Incomplete command)
Quy tắc:
Không giải thích dài dòng.
tập trung vào lỗi phần cứng, cảnh báo bất thường
Nếu log không lỗi ghi "No issues detected.".
Ngắn gọn, dễ đc, đúng format
Return only json format with English.
content: `You are a network hardware tester.
Your task is to analyze router/switch logs to determine whether the device meets hardware standards for reselling.
Focus ONLY on hardware-related problems or abnormal warnings.
Software or configuration issues (e.g., port up/down, admin down, invalid commands, CLI errors, licensing messages) should be ignored unless they indicate hardware failure.
OUTPUT FORMAT (must follow exactly):
{
"issue": [ "problem 1", "problem 2", ... ],
"summary": "short summary under 30 words"
}
RULES:
- Summaries must be in English.
- Each issue must be one short line.
- If the log contains no hardware issues, output: { "issue": ["No issues detected."], "summary": "No hardware issues found." }
- Keep responses concise, readable, and strictly in JSON format.
- Do NOT add explanations outside the JSON.
- Your job is to detect hardware faults, missing components, overheating, failing modules, PSU issues, sensor anomalies, SIM/card missing, modem errors, transceiver issues, POST/diagnostics failures, etc.
The log to analyze will be provided after this prompt.
Here is the log:

View File

@ -73,13 +73,13 @@ export default class SwitchController {
this.isEnable = false
this.onData(this.portGroups, this.status)
if (this.retryConnect <= 5) {
await this.sleep(15000)
console.log('Retry connect times', this.retryConnect)
this.retryConnect += 1
await this.reconnect()
} else {
this.retryConnect = 0
return
}
await this.sleep(15000)
console.log('SWITCH Retry connect times', this.retryConnect)
this.retryConnect += 1
await this.reconnect()
}
private _handleError(err: Error & { code?: string }) {
@ -145,7 +145,8 @@ export default class SwitchController {
}
public disconnect() {
this.socket.end()
// this.socket.end()
this.socket.destroy()
this.status = 'DISCONNECTED'
this.isEnable = false
}

View File

@ -1,6 +1,7 @@
import fs from 'node:fs'
import path from 'node:path'
import nodeMailer from 'nodemailer'
import zulip from 'zulip-js'
type DetectAI = {
status: string[]
@ -18,6 +19,7 @@ type InputData = {
// Types
type SendMailResponse = string
type SendMessageType = 'stream' | 'private'
/**
* Function to clean up unwanted characters from the output data.
@ -247,3 +249,53 @@ export function sendMessageToMail(
})
})
}
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)
})
}
})
})
}

View File

@ -0,0 +1,17 @@
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'stations'
async up() {
this.schema.alterTable(this.tableName, (table) => {
table.boolean('is_active').defaultTo(true)
})
}
async down() {
this.schema.alterTable(this.tableName, (table) => {
table.dropColumn('is_active')
})
}
}

View File

@ -0,0 +1,17 @@
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'scenarios'
async up() {
this.schema.alterTable(this.tableName, (table) => {
table.boolean('send_result').defaultTo(false)
})
}
async down() {
this.schema.alterTable(this.tableName, (table) => {
table.dropColumn('send_result')
})
}
}

View File

@ -24,7 +24,8 @@
"nodemailer": "^7.0.9",
"reflect-metadata": "^0.2.2",
"socket.io": "^4.8.1",
"xregexp": "^5.1.2"
"xregexp": "^5.1.2",
"zulip-js": "^2.1.0"
},
"devDependencies": {
"@adonisjs/assembler": "^7.8.2",
@ -1417,6 +1418,15 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/runtime-corejs3": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz",
@ -6149,6 +6159,15 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ini": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz",
"integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==",
"license": "ISC",
"engines": {
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/interpret": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
@ -6320,6 +6339,63 @@
"devOptional": true,
"license": "ISC"
},
"node_modules/isomorphic-fetch": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz",
"integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.6.1",
"whatwg-fetch": "^3.4.1"
}
},
"node_modules/isomorphic-form-data": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isomorphic-form-data/-/isomorphic-form-data-2.0.0.tgz",
"integrity": "sha512-TYgVnXWeESVmQSg4GLVbalmQ+B4NPi/H4eWxqALKj63KsUrcu301YDjBqaOw3h+cbak7Na4Xyps3BiptHtxTfg==",
"license": "MIT",
"dependencies": {
"form-data": "^2.3.2"
}
},
"node_modules/isomorphic-form-data/node_modules/form-data": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
"integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.35",
"safe-buffer": "^5.2.1"
},
"engines": {
"node": ">= 0.12"
}
},
"node_modules/isomorphic-form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/isomorphic-form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/jest-diff": {
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz",
@ -6964,6 +7040,26 @@
"integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-releases": {
"version": "2.0.26",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz",
@ -8724,6 +8820,12 @@
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/truncatise": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/truncatise/-/truncatise-0.0.8.tgz",
@ -9025,6 +9127,28 @@
"node": ">= 0.8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-fetch": {
"version": "3.6.20",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
"license": "MIT"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -9265,6 +9389,18 @@
"engines": {
"node": ">= 0.6"
}
},
"node_modules/zulip-js": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/zulip-js/-/zulip-js-2.1.0.tgz",
"integrity": "sha512-kLdxzJZ/FvWHBotUJl7LXCHIkShTjy1FUk5HAWfsal1TM+hw0atCZwgasCpvFDBj01y+39ZEZXgjePaie74Xhg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.7",
"ini": "^5.0.0",
"isomorphic-fetch": "^3.0.0",
"isomorphic-form-data": "2.0.0"
}
}
}
}

View File

@ -67,7 +67,8 @@
"nodemailer": "^7.0.9",
"reflect-metadata": "^0.2.2",
"socket.io": "^4.8.1",
"xregexp": "^5.1.2"
"xregexp": "^5.1.2",
"zulip-js": "^2.1.0"
},
"hotHook": {
"boundaries": [

View File

@ -11,7 +11,13 @@ import { CustomServer, CustomSocket } from '../app/ultils/types.js'
import Line from '#models/line'
import Station from '#models/station'
import APCController from '#services/apc_connection'
import { appendLog, cleanData, sendMessageToMail, sleep } from '../app/ultils/helper.js'
import {
appendLog,
cleanData,
sendMessageToMail,
sendMessageToZulip,
sleep,
} from '../app/ultils/helper.js'
import SwitchController from '#services/switch_connection'
import redis from '@adonisjs/redis/services/main'
import axios from 'axios'
@ -554,6 +560,7 @@ export class WebSocketIo {
const results = await this.waitUntilAllReady(lineIds)
const tableHTML = this.generateTable(results)
const zulipMess = this.generateZulipMessage(results)
const timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
const dataFormat = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm:ss')
@ -562,14 +569,20 @@ export class WebSocketIo {
process.env.LINK_WIKI || 'https://logs.danielvu.com/api/wiki/page/insert?title=Dev_test'
await axios.post(linkWiki, {
data: tableHTML,
titleAuto: `[DPELP] Report AUTO - ${stationName} - ` + dataFormat,
titleAuto: `[DPELP] - ${stationName} - ` + dataFormat,
})
await sendMessageToMail(
'andrew.ng@apactech.io',
`[DPELP] Report AUTO - ${stationName} - ${dataFormat}`,
`[DPELP] - ${stationName} - ${dataFormat}`,
tableHTML,
['ips@ipsupply.com.au', 'kay@ipsupply.com.au', 'joseph@apactech.io']
)
await sendMessageToZulip(
'stream',
'ATC_Report',
station.name,
`\n\n---\n[DPELP] - ${stationName} - ${dataFormat}\n` + zulipMess
)
} catch (error) {
console.log(error)
}
@ -655,6 +668,10 @@ export class WebSocketIo {
clearInterval(this.intervalMap[`${lineId}`])
delete this.intervalMap[`${lineId}`]
}
if (this.intervalKeepConnect[`${lineId}`]) {
clearInterval(this.intervalKeepConnect[`${lineId}`])
delete this.intervalKeepConnect[`${lineId}`]
}
}, timeout)
this.intervalMap[`${lineId}`] = interval
@ -1067,6 +1084,44 @@ export class WebSocketIo {
return html
}
/**
* Generates a Zulip-compatible Markdown table string from the results array.
* Uses <br> to force line breaks within the License cell.
* @param {Array<Object>} results - The array of data objects.
* @returns {string} The Markdown table string.
*/
generateZulipMessage(results: any[]) {
let msg = ''
for (const item of results) {
if (!item) continue
const licenses = Array.isArray(item.license)
? [...new Set(item.license)]
: item.license
? [item.license]
: []
const issues = item.issues || []
msg += `**Line ${item.line || '?'}${item.pid || ''}${item.vid ? ` (${item.vid})` : ''} `
msg += `**SN:** **${item.sn || ''}** \n`
msg += '```' + `\n`
msg += `MAC: ${item.mac || ''} \n`
msg += `IOS: ${item.ios || ''} \n`
msg += `License: ${licenses.join(', ') || ''} \n`
msg += `Issues: \n`
if (issues.length) {
for (const i of issues) msg += `${i} \n`
} else msg += `• No issues detected.\n`
msg += '```\n'
}
return msg
}
private async connectStation(station: Station) {
try {
const stationConn = new StationConnection({
@ -1081,6 +1136,7 @@ export class WebSocketIo {
await stationConn.connect()
stationConn.writeCommand('\r\n')
this.setTimeoutConnect(station.id, stationConn)
this.keepConnectStation(station.id)
} catch (error) {
console.log(error)
}
@ -1120,4 +1176,21 @@ export class WebSocketIo {
console.log('Station connect error:', err.message)
}
}
private keepConnectStation = (id: number) => {
if (this.intervalKeepConnect[`${id}`]) {
clearInterval(this.intervalKeepConnect[`${id}`])
delete this.intervalKeepConnect[`${id}`]
}
const interval = setInterval(async () => {
const station = this.stationMap.get(id)
if (station) {
await this.handleStationOperation(id, async (stationCon) => {
stationCon.writeCommand(`\r\n`)
})
}
}, 120000)
this.intervalKeepConnect[`${id}`] = interval
}
}

View File

@ -111,6 +111,7 @@ export default function InputHistory({
return (
<TextInput
ref={inputRef}
style={{
width: "30vw",
boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)",