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 }) @column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime declare updatedAt: DateTime
@column()
declare send_result: boolean
} }

View File

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

View File

@ -165,7 +165,7 @@ export default class LineConnection {
if (!this.config.inventory) if (!this.config.inventory)
this.outputInventory = this.outputInventory.slice(-3000) + message this.outputInventory = this.outputInventory.slice(-3000) + message
} }
if (message.includes('--More--')) this.writeCommand(' ') if (data.toString().includes('--More--')) this.writeCommand(' ')
// let output = cleanData(message) // let output = cleanData(message)
// console.log(`📨 [${this.config.port}] ${message}`) // console.log(`📨 [${this.config.port}] ${message}`)
@ -735,19 +735,23 @@ export default class LineConnection {
messages: [ messages: [
{ {
role: 'user', role: 'user',
content: `Bạn là chuyên gia phân tích log thiết bị mạng. content: `You are a network hardware tester.
Hãy phân tích đoạn log sau xuất kết quả theo đúng format: Your task is to analyze router/switch logs to determine whether the device meets hardware standards for reselling.
Yêu cầu đu ra đúng cấu trúc: Focus ONLY on hardware-related problems or abnormal warnings.
issue: 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.
(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) OUTPUT FORMAT (must follow exactly):
{
Quy tắc: "issue": [ "problem 1", "problem 2", ... ],
Không giải thích dài dòng. "summary": "short summary under 30 words"
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.". RULES:
- Summaries must be in English.
Ngắn gọn, dễ đc, đúng format - Each issue must be one short line.
Return only json format with English. - 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: Here is the log:

View File

@ -73,13 +73,13 @@ export default class SwitchController {
this.isEnable = false this.isEnable = false
this.onData(this.portGroups, this.status) this.onData(this.portGroups, this.status)
if (this.retryConnect <= 5) { 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 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 }) { private _handleError(err: Error & { code?: string }) {
@ -145,7 +145,8 @@ export default class SwitchController {
} }
public disconnect() { public disconnect() {
this.socket.end() // this.socket.end()
this.socket.destroy()
this.status = 'DISCONNECTED' this.status = 'DISCONNECTED'
this.isEnable = false this.isEnable = false
} }

View File

@ -1,6 +1,7 @@
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import nodeMailer from 'nodemailer' import nodeMailer from 'nodemailer'
import zulip from 'zulip-js'
type DetectAI = { type DetectAI = {
status: string[] status: string[]
@ -18,6 +19,7 @@ type InputData = {
// Types // Types
type SendMailResponse = string type SendMailResponse = string
type SendMessageType = 'stream' | 'private'
/** /**
* Function to clean up unwanted characters from the output data. * 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", "nodemailer": "^7.0.9",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"xregexp": "^5.1.2" "xregexp": "^5.1.2",
"zulip-js": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {
"@adonisjs/assembler": "^7.8.2", "@adonisjs/assembler": "^7.8.2",
@ -1417,6 +1418,15 @@
"node": ">=6.9.0" "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": { "node_modules/@babel/runtime-corejs3": {
"version": "7.28.4", "version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz",
@ -6149,6 +6159,15 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC" "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": { "node_modules/interpret": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
@ -6320,6 +6339,63 @@
"devOptional": true, "devOptional": true,
"license": "ISC" "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": { "node_modules/jest-diff": {
"version": "30.2.0", "version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz",
@ -6964,6 +7040,26 @@
"integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==", "integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==",
"license": "MIT" "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": { "node_modules/node-releases": {
"version": "2.0.26", "version": "2.0.26",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz",
@ -8724,6 +8820,12 @@
"url": "https://github.com/sponsors/Borewit" "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": { "node_modules/truncatise": {
"version": "0.0.8", "version": "0.0.8",
"resolved": "https://registry.npmjs.org/truncatise/-/truncatise-0.0.8.tgz", "resolved": "https://registry.npmjs.org/truncatise/-/truncatise-0.0.8.tgz",
@ -9025,6 +9127,28 @@
"node": ">= 0.8" "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": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -9265,6 +9389,18 @@
"engines": { "engines": {
"node": ">= 0.6" "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", "nodemailer": "^7.0.9",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"xregexp": "^5.1.2" "xregexp": "^5.1.2",
"zulip-js": "^2.1.0"
}, },
"hotHook": { "hotHook": {
"boundaries": [ "boundaries": [

View File

@ -11,7 +11,13 @@ import { CustomServer, CustomSocket } from '../app/ultils/types.js'
import Line from '#models/line' import Line from '#models/line'
import Station from '#models/station' import Station from '#models/station'
import APCController from '#services/apc_connection' 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 SwitchController from '#services/switch_connection'
import redis from '@adonisjs/redis/services/main' import redis from '@adonisjs/redis/services/main'
import axios from 'axios' import axios from 'axios'
@ -554,6 +560,7 @@ export class WebSocketIo {
const results = await this.waitUntilAllReady(lineIds) const results = await this.waitUntilAllReady(lineIds)
const tableHTML = this.generateTable(results) const tableHTML = this.generateTable(results)
const zulipMess = this.generateZulipMessage(results)
const timeZone = process.env.TIME_ZONE || 'Australia/Sydney' const timeZone = process.env.TIME_ZONE || 'Australia/Sydney'
const dataFormat = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm:ss') 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' process.env.LINK_WIKI || 'https://logs.danielvu.com/api/wiki/page/insert?title=Dev_test'
await axios.post(linkWiki, { await axios.post(linkWiki, {
data: tableHTML, data: tableHTML,
titleAuto: `[DPELP] Report AUTO - ${stationName} - ` + dataFormat, titleAuto: `[DPELP] - ${stationName} - ` + dataFormat,
}) })
await sendMessageToMail( await sendMessageToMail(
'andrew.ng@apactech.io', 'andrew.ng@apactech.io',
`[DPELP] Report AUTO - ${stationName} - ${dataFormat}`, `[DPELP] - ${stationName} - ${dataFormat}`,
tableHTML, tableHTML,
['ips@ipsupply.com.au', 'kay@ipsupply.com.au', 'joseph@apactech.io'] ['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) { } catch (error) {
console.log(error) console.log(error)
} }
@ -655,6 +668,10 @@ export class WebSocketIo {
clearInterval(this.intervalMap[`${lineId}`]) clearInterval(this.intervalMap[`${lineId}`])
delete this.intervalMap[`${lineId}`] delete this.intervalMap[`${lineId}`]
} }
if (this.intervalKeepConnect[`${lineId}`]) {
clearInterval(this.intervalKeepConnect[`${lineId}`])
delete this.intervalKeepConnect[`${lineId}`]
}
}, timeout) }, timeout)
this.intervalMap[`${lineId}`] = interval this.intervalMap[`${lineId}`] = interval
@ -1067,6 +1084,44 @@ export class WebSocketIo {
return html 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) { private async connectStation(station: Station) {
try { try {
const stationConn = new StationConnection({ const stationConn = new StationConnection({
@ -1081,6 +1136,7 @@ export class WebSocketIo {
await stationConn.connect() await stationConn.connect()
stationConn.writeCommand('\r\n') stationConn.writeCommand('\r\n')
this.setTimeoutConnect(station.id, stationConn) this.setTimeoutConnect(station.id, stationConn)
this.keepConnectStation(station.id)
} catch (error) { } catch (error) {
console.log(error) console.log(error)
} }
@ -1120,4 +1176,21 @@ export class WebSocketIo {
console.log('Station connect error:', err.message) 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 ( return (
<TextInput <TextInput
ref={inputRef}
style={{ style={{
width: "30vw", width: "30vw",
boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)", boxShadow: "0px 0px 10px rgba(0, 0, 0, 0.1)",