UNPKG

survey-mcp-server

Version:

Survey management server handling survey creation, response collection, analysis, and reporting with database access for data management

502 lines 24.1 kB
import { chromium } from 'playwright'; import { logger } from './logger.js'; import fs from 'fs'; import path from 'path'; export class BrowserAutomation { constructor(browserConfig = {}) { this.browserConfig = browserConfig; this.browser = null; this.context = null; this.page = null; this.downloadPath = browserConfig.downloadPath || path.join(process.cwd(), 'downloads'); this.ensureDownloadDirectory(); } ensureDownloadDirectory() { if (!fs.existsSync(this.downloadPath)) { fs.mkdirSync(this.downloadPath, { recursive: true }); } } async initialize() { try { this.browser = await chromium.launch({ headless: this.browserConfig.headless ?? false, timeout: this.browserConfig.timeout ?? 30000 }); this.context = await this.browser.newContext({ viewport: this.browserConfig.viewport ?? { width: 1280, height: 800 }, acceptDownloads: true }); this.page = await this.context.newPage(); // Set default timeout this.page.setDefaultTimeout(this.browserConfig.timeout ?? 30000); logger.info('Browser automation initialized successfully'); } catch (error) { logger.error('Failed to initialize browser automation:', error); throw error; } } async cleanup() { try { if (this.page) await this.page.close(); if (this.context) await this.context.close(); if (this.browser) await this.browser.close(); logger.info('Browser automation cleaned up successfully'); } catch (error) { logger.error('Error during browser cleanup:', error); } } getPage() { if (!this.page) { throw new Error('Browser not initialized. Call initialize() first.'); } return this.page; } async waitForDownload(timeout = 30000) { if (!this.page) throw new Error('Page not initialized'); const downloadPromise = this.page.waitForEvent('download', { timeout }); const download = await downloadPromise; const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `${download.suggestedFilename()}_${timestamp}`; const filepath = path.join(this.downloadPath, filename); await download.saveAs(filepath); logger.info(`File downloaded: ${filepath}`); return filepath; } async solveCaptchaWithOpenAI(captchaImageSelector) { if (!this.page) throw new Error('Page not initialized'); try { // Take screenshot of captcha const captchaElement = await this.page.locator(captchaImageSelector); const captchaScreenshot = await captchaElement.screenshot(); // Use OpenAI to solve captcha (placeholder - would need actual OpenAI integration) logger.info('Captcha solving requested - placeholder implementation'); return 'CAPTCHA_SOLUTION_PLACEHOLDER'; } catch (error) { logger.error('Failed to solve captcha:', error); throw error; } } async handlePopup(action) { if (!this.page) throw new Error('Page not initialized'); const popupPromise = this.page.waitForEvent('popup'); await action(); const popup = await popupPromise; logger.info('Popup window opened'); return popup; } async waitForNetworkIdle(timeout = 10000) { if (!this.page) throw new Error('Page not initialized'); await this.page.waitForLoadState('networkidle', { timeout }); } async takeScreenshot(filename) { if (!this.page) throw new Error('Page not initialized'); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const screenshotPath = path.join(this.downloadPath, filename || `screenshot_${timestamp}.png`); await this.page.screenshot({ path: screenshotPath, fullPage: true }); logger.info(`Screenshot saved: ${screenshotPath}`); return screenshotPath; } } // Classification Society specific automation classes export class CCSAutomation extends BrowserAutomation { async downloadSurveyStatus(vesselName, credentials) { const page = this.getPage(); const downloadDir = path.join(this.downloadPath, 'CCS'); if (!fs.existsSync(downloadDir)) { fs.mkdirSync(downloadDir, { recursive: true }); } try { logger.info(`Starting CCS survey status download for vessel: ${vesselName}`); // Navigate to CCS portal await page.goto('https://www.ccs-service.net/loginNewEn.jsp'); await page.waitForLoadState('networkidle'); // Login process await page.fill('input[name="username"]', credentials.username); await page.fill('input[name="password"]', credentials.password); // Handle captcha if present const captchaExists = await page.locator('img[id*="captcha"]').count() > 0; if (captchaExists) { const captchaSolution = await this.solveCaptchaWithOpenAI('img[id*="captcha"]'); await page.fill('input[name="captcha"]', captchaSolution); } await page.click('input[type="submit"]'); await this.waitForNetworkIdle(); // Navigate to fleet section await page.click('a[href*="fleet"]'); await this.waitForNetworkIdle(); // Search for vessel await page.fill('input[name="vesselName"]', vesselName); await page.click('button[type="submit"]'); await this.waitForNetworkIdle(); // Click on vessel link await page.click(`a:has-text("${vesselName}")`); await this.waitForNetworkIdle(); // Download survey status const downloadPromise = this.waitForDownload(); await page.click('a[href*="survey-status"]'); const downloadPath = await downloadPromise; logger.info(`CCS survey status downloaded successfully for ${vesselName}`); return downloadPath; } catch (error) { logger.error(`Failed to download CCS survey status for ${vesselName}:`, error); await this.takeScreenshot(`ccs_error_${vesselName.replace(/\s+/g, '_')}.png`); throw error; } } } export class NKAutomation extends BrowserAutomation { async downloadSurveyStatus(vesselName, credentials) { const page = this.getPage(); const downloadDir = path.join(this.downloadPath, 'NK'); if (!fs.existsSync(downloadDir)) { fs.mkdirSync(downloadDir, { recursive: true }); } try { logger.info(`Starting NK survey status download for vessel: ${vesselName}`); // Navigate to NK portal await page.goto('https://portal.classnk.or.jp/portal/'); await page.waitForLoadState('networkidle'); // Login process await page.fill('input[name="userId"]', credentials.username); await page.fill('input[name="password"]', credentials.password); // Handle captcha if present const captchaExists = await page.locator('img[alt="CAPTCHA"]').count() > 0; if (captchaExists) { const captchaSolution = await this.solveCaptchaWithOpenAI('img[alt="CAPTCHA"]'); await page.fill('input[name="captcha"]', captchaSolution); } await page.click('input[value="Login"]'); await this.waitForNetworkIdle(); // Navigate to NK-SHIPS await page.click('a:has-text("NK-SHIPS")'); await this.waitForNetworkIdle(); // Handle popup if it opens const popup = await this.handlePopup(async () => { await page.click('a[href*="ships"]'); }); // Search for vessel in popup await popup.fill('input[name="vesselName"]', vesselName); await popup.press('input[name="vesselName"]', 'Enter'); await popup.waitForLoadState('networkidle'); // Click on vessel from search results await popup.click(`tr:has-text("${vesselName}") a`); await popup.waitForLoadState('networkidle'); // Download survey status const downloadPromise = this.waitForDownload(); await popup.click('a:has-text("Survey Status")'); const downloadPath = await downloadPromise; await popup.close(); logger.info(`NK survey status downloaded successfully for ${vesselName}`); return downloadPath; } catch (error) { logger.error(`Failed to download NK survey status for ${vesselName}:`, error); await this.takeScreenshot(`nk_error_${vesselName.replace(/\s+/g, '_')}.png`); throw error; } } } export class KRAutomation extends BrowserAutomation { async downloadSurveyStatus(vesselName, credentials) { const page = this.getPage(); const downloadDir = path.join(this.downloadPath, 'KR'); if (!fs.existsSync(downloadDir)) { fs.mkdirSync(downloadDir, { recursive: true }); } try { logger.info(`Starting KR survey status download for vessel: ${vesselName}`); // Navigate to KR e-fleet portal await page.goto('https://e-fleet.krs.co.kr/View/Login/CheckMember_New_V2.aspx'); await page.waitForLoadState('networkidle'); // First authentication step await page.fill('input[name="txtId"]', credentials.username); await page.fill('input[name="txtPwd"]', credentials.password); await page.click('input[name="btnLogin"]'); await this.waitForNetworkIdle(); // Email authentication step if (credentials.email && credentials.emailPassword) { await page.fill('input[name="email"]', credentials.email); await page.fill('input[name="emailPwd"]', credentials.emailPassword); await page.click('input[name="btnEmailLogin"]'); await this.waitForNetworkIdle(); } // Navigate to VESSEL section await page.click('a:has-text("VESSEL")'); await this.waitForNetworkIdle(); // Search for vessel and get CLASS number via API const response = await page.evaluate(async (vesselName) => { const response = await fetch('/View/VESSEL/DataHandler/Vessel_List.ashx', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `vesselName=${encodeURIComponent(vesselName)}` }); return response.json(); }, vesselName); const classNumber = response.data?.[0]?.CLASS_NO; if (!classNumber) { throw new Error(`Could not find CLASS number for vessel: ${vesselName}`); } // Download using CLASS number const downloadPromise = this.waitForDownload(); await page.goto(`https://e-fleet.krs.co.kr/View/eShip/PopUp/FileDownPage2.aspx?CLASS_NO=${classNumber}`); const downloadPath = await downloadPromise; logger.info(`KR survey status downloaded successfully for ${vesselName}`); return downloadPath; } catch (error) { logger.error(`Failed to download KR survey status for ${vesselName}:`, error); await this.takeScreenshot(`kr_error_${vesselName.replace(/\s+/g, '_')}.png`); throw error; } } } export class DNVAutomation extends BrowserAutomation { async downloadSurveyStatus(vesselName, credentials) { const page = this.getPage(); const downloadDir = path.join(this.downloadPath, 'DNV'); if (!fs.existsSync(downloadDir)) { fs.mkdirSync(downloadDir, { recursive: true }); } try { logger.info(`Starting DNV survey status download for vessel: ${vesselName}`); // Navigate to DNV Veracity portal await page.goto('https://www.veracity.com/auth/login'); await page.waitForLoadState('networkidle'); // Login to Veracity await page.fill('input[name="username"]', credentials.username); await page.fill('input[name="password"]', credentials.password); await page.click('button[type="submit"]'); await this.waitForNetworkIdle(); // Navigate to My services await page.click('a:has-text("My services")'); await this.waitForNetworkIdle(); // Access Fleet Status application (opens in popup) const popup = await this.handlePopup(async () => { await page.click('a:has-text("Fleet Status")'); }); // Navigate within popup to maritime.dnv.com await popup.goto('https://maritime.dnv.com/Fleet'); await popup.waitForLoadState('networkidle'); // Accept cookies const cookieButton = popup.locator('button:has-text("Accept")'); if (await cookieButton.count() > 0) { await cookieButton.click(); } // Navigate to Vessel list await popup.click('a:has-text("Vessels")'); await popup.waitForLoadState('networkidle'); // Search for vessel await popup.fill('input[placeholder*="vessel"]', vesselName); await popup.press('input[placeholder*="vessel"]', 'Enter'); await popup.waitForLoadState('networkidle'); // Select vessel await popup.click(`tr:has-text("${vesselName}") a`); await popup.waitForLoadState('networkidle'); // Verify vessel is in DNV class const statusText = await popup.textContent('.vessel-status'); if (!statusText?.includes('In DNV Class In Operation')) { throw new Error(`Vessel ${vesselName} is not in DNV Class In Operation`); } // Open menu and download await popup.click('button[aria-label="Menu"]'); await popup.click('a:has-text("Download class status")'); // Select "With Memorandum to Owner" await popup.check('input[value="withMemorandum"]'); const downloadPromise = this.waitForDownload(); await popup.click('button:has-text("Download PDF")'); const downloadPath = await downloadPromise; await popup.close(); logger.info(`DNV survey status downloaded successfully for ${vesselName}`); return downloadPath; } catch (error) { logger.error(`Failed to download DNV survey status for ${vesselName}:`, error); await this.takeScreenshot(`dnv_error_${vesselName.replace(/\s+/g, '_')}.png`); throw error; } } } export class LRAutomation extends BrowserAutomation { async downloadSurveyStatus(vesselName, credentials) { const page = this.getPage(); const downloadDir = path.join(this.downloadPath, 'LR'); if (!fs.existsSync(downloadDir)) { fs.mkdirSync(downloadDir, { recursive: true }); } try { logger.info(`Starting LR survey status download for vessel: ${vesselName}`); // Navigate to LR client portal await page.goto('https://www.lr.org/en/client-support/sign-in-client-portal/'); await page.waitForLoadState('networkidle'); // Accept cookies const cookieButton = page.locator('button:has-text("Accept")'); if (await cookieButton.count() > 0) { await cookieButton.click(); } // Click Login to open popup const loginPopup = await this.handlePopup(async () => { await page.click('a:has-text("Login")'); }); // Login in popup await loginPopup.fill('input[name="email"]', credentials.email || ''); await loginPopup.fill('input[name="password"]', credentials.password); await loginPopup.click('button[type="submit"]'); await loginPopup.waitForLoadState('networkidle'); // Navigate to Fleet section await loginPopup.click('a:has-text("Fleet")'); await loginPopup.waitForLoadState('networkidle'); // Access LR Class Direct (opens new popup) const classDirectPopup = await this.handlePopup(async () => { await loginPopup.click('a:has-text("LR Class Direct")'); }); // Search for vessel await classDirectPopup.fill('input[placeholder*="vessel"]', vesselName); await classDirectPopup.press('input[placeholder*="vessel"]', 'Enter'); await classDirectPopup.waitForLoadState('networkidle'); // Select vessel from results await classDirectPopup.click(`tr:has-text("${vesselName}") a`); await classDirectPopup.waitForLoadState('networkidle'); // Access Survey Status Report await classDirectPopup.click('a:has-text("Survey Status Report")'); await classDirectPopup.waitForLoadState('networkidle'); // Add all reports await classDirectPopup.click('button:has-text("Add all")'); // Download PDF const downloadPromise = this.waitForDownload(); await classDirectPopup.click('button:has-text("Download PDF")'); const downloadPath = await downloadPromise; await classDirectPopup.close(); await loginPopup.close(); logger.info(`LR survey status downloaded successfully for ${vesselName}`); return downloadPath; } catch (error) { logger.error(`Failed to download LR survey status for ${vesselName}:`, error); await this.takeScreenshot(`lr_error_${vesselName.replace(/\s+/g, '_')}.png`); throw error; } } } export class BVAutomation extends BrowserAutomation { async downloadSurveyStatus(vesselName, credentials) { const page = this.getPage(); const downloadDir = path.join(this.downloadPath, 'BV'); if (!fs.existsSync(downloadDir)) { fs.mkdirSync(downloadDir, { recursive: true }); } try { logger.info(`Starting BV survey status download for vessel: ${vesselName}`); // Navigate to BV Move platform await page.goto('https://move.bureauveritas.com/#/cms'); await page.waitForLoadState('networkidle'); // Click Connect Now await page.click('button:has-text("Connect Now")'); await page.waitForLoadState('networkidle'); // Login await page.fill('input[name="username"]', credentials.username); await page.fill('input[name="password"]', credentials.password); await page.click('button:has-text("Sign in")'); await this.waitForNetworkIdle(); // Navigate to FLEET IN SERVICE await page.click('a:has-text("FLEET IN SERVICE")'); await page.waitForLoadState('networkidle'); // Search and select vessel by exact name match const vesselElement = page.locator(`text="${vesselName}"`); await vesselElement.click(); await page.waitForLoadState('networkidle'); // Download Ship Status PDF const downloadPromise = this.waitForDownload(); await page.click('button:has-text("Download Ship Status PDF")'); // Confirm download await page.click('button:has-text("Download PDF")'); const downloadPath = await downloadPromise; logger.info(`BV survey status downloaded successfully for ${vesselName}`); return downloadPath; } catch (error) { logger.error(`Failed to download BV survey status for ${vesselName}:`, error); await this.takeScreenshot(`bv_error_${vesselName.replace(/\s+/g, '_')}.png`); throw error; } } } export class ABSAutomation extends BrowserAutomation { async downloadSurveyStatus(vesselName, credentials) { const page = this.getPage(); const downloadDir = path.join(this.downloadPath, 'ABS'); if (!fs.existsSync(downloadDir)) { fs.mkdirSync(downloadDir, { recursive: true }); } try { logger.info(`Starting ABS survey status download for vessel: ${vesselName}`); // Navigate to ABS Eagle website await page.goto('https://ww2.eagle.org/en.html'); await page.waitForLoadState('networkidle'); // Click Login await page.click('a:has-text("Login")'); await page.waitForLoadState('networkidle'); // Login await page.fill('input[name="username"]', credentials.username); await page.fill('input[name="password"]', credentials.password); await page.click('button[type="submit"]'); // Wait longer for ABS login and handle potential popups await page.waitForTimeout(40000); // Handle popup if present const popupExists = await page.locator('div[role="dialog"]').count() > 0; if (popupExists) { await page.click('button[aria-label="Close"]'); await page.waitForTimeout(20000); } // Navigate to portal dashboard await page.goto('https://www.eagle.org/portal/#/portal/dashboard'); await page.waitForLoadState('networkidle'); // Navigate to Fleet -> Vessels await page.click('a:has-text("Fleet")'); await page.waitForLoadState('networkidle'); await page.click('a:has-text("Vessels")'); await page.waitForLoadState('networkidle'); // Clear all filters const clearAllButton = page.locator('button:has-text("Clear All")'); if (await clearAllButton.count() > 0) { await clearAllButton.click(); } // Search for vessel await page.fill('input[placeholder*="vessel"]', vesselName); await page.press('input[placeholder*="vessel"]', 'Enter'); await page.waitForLoadState('networkidle'); // Select vessel await page.click(`tr:has-text("${vesselName}") a`); await page.waitForLoadState('networkidle'); // Click Vessel Status download await page.click('button:has-text("Vessel Status")'); // Configure report options await page.check('input[value="withAsset"]'); await page.check('input[value="withCompartments"]'); // Generate and download const downloadPromise = this.waitForDownload(); await page.click('button:has-text("Generate Report")'); await page.click('button:has-text("Download")'); const downloadPath = await downloadPromise; logger.info(`ABS survey status downloaded successfully for ${vesselName}`); return downloadPath; } catch (error) { logger.error(`Failed to download ABS survey status for ${vesselName}:`, error); await this.takeScreenshot(`abs_error_${vesselName.replace(/\s+/g, '_')}.png`); throw error; } } } //# sourceMappingURL=browser.js.map