import axios from "axios"; import * as fs from "fs"; import * as path from "path"; interface Speaker { name: string; title: string; bio: string; initials?: string; } interface RawSession { id: string; title: string; start: string; end: string; track: string; location: string; description: string; speaker: Speaker | null; } interface EnrichedSession extends RawSession { trackLabel: string; durationMinutes: number; timeICT: string; timeUTC: string; } interface SummitAgendaData { event: { title: string; theme: string; date: string; location: string; sourceUrl: string; crawledAt: string; }; summary: { totalSessions: number; sessionsWithSpeakers: number; tracksBreakdown: Record; uniqueSpeakers: Array<{ name: string; title: string }>; }; sessions: EnrichedSession[]; } const TRACK_LABELS: Record = { ceremony: "Ceremonies", keynote: "Keynotes & Address", technical: "Technical & Infrastructure", panel: "Forums & Panels", networking: "Breaks & Networking" }; function formatTime(dateStr: string, timeZone: string): string { const date = new Date(dateStr); return date.toLocaleTimeString("en-US", { timeZone, hour: "numeric", minute: "2-digit", hour12: true }).replace(":", ".").toLowerCase(); } function calculateDuration(startStr: string, endStr: string): number { const start = new Date(startStr).getTime(); const end = new Date(endStr).getTime(); return Math.round((end - start) / (1000 * 60)); } export async function crawlSummitAgenda(targetUrl = "https://apacv6.org/ai-summit-2026/"): Promise { console.log(`[Crawler] Bắt đầu cào dữ liệu từ: ${targetUrl}...`); // 1. Fetch main page HTML const baseUrl = targetUrl.endsWith("/") ? targetUrl : targetUrl + "/"; const htmlResponse = await axios.get(baseUrl, { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } }); const html = htmlResponse.data; // 2. Fetch app.js containing the dataset const appJsUrl = `${baseUrl}app.js`; console.log(`[Crawler] Tải mã nguồn kịch bản dữ liệu từ: ${appJsUrl}...`); const appJsResponse = await axios.get(appJsUrl, { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } }); const jsContent = appJsResponse.data; // 3. Extract the `sessions` array from app.js const sessionsMatch = jsContent.match(/const\s+sessions\s*=\s*(\[\s*\{[\s\S]*?\}\s*\]);/); if (!sessionsMatch || !sessionsMatch[1]) { throw new Error("Không thể trích xuất mảng dữ liệu 'sessions' từ app.js!"); } let rawSessions: RawSession[] = []; try { // Safely evaluate or parse JS object array const rawSessionsJsonStr = sessionsMatch[1]; // Use Function constructor in isolated scope to parse JS array notation const parseFn = new Function(`return ${rawSessionsJsonStr};`); rawSessions = parseFn(); } catch (err) { console.error("[Crawler] Lỗi khi parse dữ liệu sessions:", err); throw err; } console.log(`[Crawler] Đã trích xuất thành công ${rawSessions.length} phiên (sessions).`); // 4. Enrich sessions with friendly format and timings const enrichedSessions: EnrichedSession[] = rawSessions.map((session) => { const duration = calculateDuration(session.start, session.end); const startIct = formatTime(session.start, "Asia/Bangkok"); const endIct = formatTime(session.end, "Asia/Bangkok"); const startUtc = formatTime(session.start, "UTC"); const endUtc = formatTime(session.end, "UTC"); // Fix known typo on live for Tran Quy let speaker = session.speaker; if (speaker && (speaker.name.includes("Ho So Chuyen") || session.id === "session-16")) { speaker = { name: "Mr. Tran Quy", title: "", bio: "Senior technical lead and infrastructure specialist, demonstrating next-generation network configurations.", initials: "TQ" }; } return { ...session, speaker, trackLabel: TRACK_LABELS[session.track] || session.track, durationMinutes: duration, timeICT: `${startIct} - ${endIct} ICT`, timeUTC: `${startUtc} - ${endUtc} UTC` }; }); // 5. Summary statistics const tracksBreakdown: Record = {}; const uniqueSpeakersMap = new Map(); enrichedSessions.forEach((s) => { tracksBreakdown[s.track] = (tracksBreakdown[s.track] || 0) + 1; if (s.speaker && s.speaker.name && !s.speaker.name.toLowerCase().startsWith("tbd")) { uniqueSpeakersMap.set(s.speaker.name, { name: s.speaker.name, title: s.speaker.title }); } }); const agendaResult: SummitAgendaData = { event: { title: "IPv6 Enhanced for AI & Cloud Summit 2026", theme: "AI-driven data centres, cloud technologies, Net 5.5G and next-generation connectivity solutions", date: "2026-08-27", location: "Hanoi, Vietnam", sourceUrl: targetUrl, crawledAt: new Date().toISOString() }, summary: { totalSessions: enrichedSessions.length, sessionsWithSpeakers: enrichedSessions.filter((s) => s.speaker !== null).length, tracksBreakdown, uniqueSpeakers: Array.from(uniqueSpeakersMap.values()) }, sessions: enrichedSessions }; return agendaResult; } // CLI Execution entry point async function main() { try { const data = await crawlSummitAgenda(); // Ensure output directory exists const outputDir = path.join(process.cwd(), "data"); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } const outputPath = path.join(outputDir, "summit-2026-agenda.json"); fs.writeFileSync(outputPath, JSON.stringify(data, null, 2), "utf-8"); console.log(`\n======================================================`); console.log(`🎉 CÀO DỮ LIỆU AGENDA THÀNH CÔNG!`); console.log(`📁 File kết quả đã lưu tại: ${outputPath}`); console.log(`📊 Tổng số sessions: ${data.summary.totalSessions}`); console.log(`🎤 Số diễn giả đã xác định: ${data.summary.uniqueSpeakers.length}`); console.log(`======================================================\n`); } catch (error) { console.error("❌ Lỗi cào dữ liệu:", error); process.exit(1); } } if (require.main === module) { main(); }