v2: two-phase doc crawler + web UI + server.py static file serving

- crawl_hpe_v2.js: two-phase (discover versions + sub-pages: features/resolved/known/regulatory)
- crawl_hpe.js: v1 single-phase
- hpe-aos10-versions.json: 60 versions cache
- server.py:
  - /url2pdf-tools/ now serves index.html (web UI)
  - /url2pdf-tools/<file> serves from /opt/url2pdf/ (with path traversal protection)
  - /url2pdf/crawl uses v2 crawler (was inline CRAWLER_JS hardcoded for Trend Micro)
- index.html: new web UI (paste URL → crawl → download)
This commit is contained in:
IT狗
2026-07-24 18:10:22 +08:00
parent 0d3e40f797
commit e6116ecc7d
5 changed files with 854 additions and 11 deletions
+158
View File
@@ -0,0 +1,158 @@
// Crawl HPE Aruba Networking AOS 10.x Release Notes
// Uses puppeteer-core + existing Chrome for Testing
const puppeteer = require('puppeteer-core');
const fs = require('fs');
const CHROME_PATH = '/Users/admin/.cache/puppeteer/chrome/mac_arm-148.0.7778.97/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing';
const BASE = process.argv[2] || 'https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/all-releases.htm';
const OUT_DIR = '/tmp/hpe-aruba-aos10-rn';
const URLS_FILE = OUT_DIR + '/urls.json';
const MD_FILE = OUT_DIR + '/all-releases.md';
if (!fs.existsSync(OUT_DIR)) fs.mkdirSync(OUT_DIR, { recursive: true });
const CHROME_ARGS = ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--disable-blink-features=AutomationControlled'];
const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36';
const EXTRA_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br'
};
async function setupPage(browser) {
const page = await browser.newPage();
await page.setUserAgent(USER_AGENT);
await page.setExtraHTTPHeaders(EXTRA_HEADERS);
return page;
}
async function getAllUrls() {
if (fs.existsSync(URLS_FILE)) {
const urls = JSON.parse(fs.readFileSync(URLS_FILE, 'utf8'));
console.log(`📋 ${urls.length} URLs (cached)`);
return urls;
}
const browser = await puppeteer.launch({
executablePath: CHROME_PATH,
headless: true,
args: CHROME_ARGS
});
const page = await setupPage(browser);
await page.goto(BASE, { waitUntil: 'networkidle2', timeout: 60000 });
await new Promise(r => setTimeout(r, 3000));
const baseUrl = new URL(BASE);
const articleDir = baseUrl.pathname.substring(0, baseUrl.pathname.lastIndexOf('/') + 1);
const allLinks = await page.evaluate(() => {
const links = [];
document.querySelectorAll('a[href]').forEach(a => {
const text = a.textContent.trim();
if (text.length > 2) links.push({ href: a.href, text: text.slice(0, 200) });
});
return links;
});
await browser.close();
const seen = new Set();
const docPages = [];
for (const link of allLinks) {
try {
const u = new URL(link.href);
if (u.hostname !== baseUrl.hostname) continue;
if (!u.pathname.startsWith(articleDir)) continue;
if (u.hash) continue;
if (/javascript:|^#|^\?/i.test(link.href)) continue;
const fn = u.pathname.split('/').pop();
if (!fn || fn.length < 3) continue;
if (!seen.has(fn)) {
seen.add(fn);
docPages.push({ href: link.href, text: link.text });
}
} catch (e) {}
}
docPages.unshift({ href: BASE, text: 'All Releases (Index)' });
fs.writeFileSync(URLS_FILE, JSON.stringify(docPages, null, 2));
console.log(`📋 ${docPages.length} URLs found`);
return docPages;
}
async function extractPage(page, href) {
await page.goto(href, { waitUntil: 'networkidle2', timeout: 30000 });
await new Promise(r => setTimeout(r, 600));
return await page.evaluate(() => {
const mainSel = ['article', '.content', '.main-content', '.doc-content', 'main', '#content', '.topic-content', '.MCWebHelpFramesetLink'];
let main = document.body;
for (const sel of mainSel) {
const el = document.querySelector(sel);
if (el) { main = el; break; }
}
const h1 = main.querySelector('h1, h2');
const title = h1 ? h1.textContent.trim() : document.title.trim();
const blocks = [];
main.querySelectorAll('h1, h2, h3, h4, p, li, pre, code, table').forEach(el => {
const txt = el.textContent.trim();
if (!txt || txt.length < 3) return;
const tag = el.tagName;
if (tag.match(/^H[1-4]$/)) {
blocks.push('\n' + '#'.repeat(parseInt(tag[1]) + 1) + ' ' + txt + '\n');
} else if (tag === 'LI') {
blocks.push('- ' + txt);
} else if (tag === 'PRE' || tag === 'CODE') {
blocks.push('\n```\n' + txt + '\n```\n');
} else if (tag === 'TABLE') {
const rows = [];
el.querySelectorAll('tr').forEach(tr => {
const cells = Array.from(tr.querySelectorAll('th, td')).map(c => c.textContent.trim()).join(' | ');
if (cells) rows.push('| ' + cells + ' |');
});
if (rows.length) blocks.push('\n' + rows.join('\n') + '\n');
} else {
blocks.push(txt);
}
});
return { title, body: blocks.join('\n\n') };
});
}
async function crawl() {
const urls = await getAllUrls();
console.log(`\n📥 Starting crawl of ${urls.length} pages...\n`);
fs.writeFileSync(MD_FILE, `# HPE Aruba Networking — AOS 10.x Release Notes\n\n> Source: ${BASE}\n> Pages: ${urls.length}\n> Crawled: ${new Date().toISOString()}\n\n---\n\n`);
const browser = await puppeteer.launch({
executablePath: CHROME_PATH,
headless: true,
args: CHROME_ARGS
});
const page = await setupPage(browser);
let ok = 0, fail = 0;
for (let i = 0; i < urls.length; i++) {
const { href, text } = urls[i];
try {
const content = await extractPage(page, href);
const md = `## ${i + 1}. ${content.title}\n\n_Source: ${href}_\n\n${content.body || '_no content_'}\n\n---\n\n`;
fs.appendFileSync(MD_FILE, md);
ok++;
console.log(`✅ [${i+1}/${urls.length}] ${content.title.slice(0, 60).replace(/\n/g, ' ')}`);
} catch (e) {
fail++;
const md = `## ${i + 1}. ${text}\n\n_❌ Error: ${e.message}_\n\n_Source: ${href}_\n\n---\n\n`;
fs.appendFileSync(MD_FILE, md);
console.log(`❌ [${i+1}/${urls.length}] ${e.message.slice(0, 80)}`);
}
}
await browser.close();
console.log(`\n📊 Done: ${ok} OK, ${fail} failed`);
console.log(`📁 Output: ${MD_FILE}`);
}
crawl().catch(err => { console.error(err); process.exit(1); });
+251
View File
@@ -0,0 +1,251 @@
// Crawl HPE Aruba Networking AOS 10.x Release Notes - v2
// Phase 1: discover all version overview pages
// Phase 2: for each version, fetch features/resolved/known sub-pages
const puppeteer = require('puppeteer-core');
const fs = require('fs');
// Auto-detect Chrome across Mac / Linux / Windows
function findChrome() {
const fs = require('fs');
const path = require('path');
const homedir = require('os').homedir();
const candidates = [
// macOS (local dev)
path.join(homedir, '.cache/puppeteer/chrome/mac_arm-148.0.7778.97/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'),
path.join(homedir, '.cache/puppeteer/chrome/mac_arm-148.0.7778.167/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'),
// Linux
'/usr/bin/google-chrome',
'/usr/bin/chromium-browser',
'/usr/bin/chromium',
// Docker puppeteer cache
path.join(homedir, '.cache/puppeteer/chrome/'),
];
for (const c of candidates) {
if (fs.existsSync(c)) return c;
}
return null;
}
const CHROME_PATH = findChrome();
if (!CHROME_PATH) {
console.error('No Chrome found. Try: apt install chromium / google-chrome-stable');
process.exit(1);
}
console.log('Using Chrome:', CHROME_PATH);
const BASE = process.argv[2] || 'https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/all-releases.htm';
const OUT_DIR = '/tmp/hpe-aruba-aos10-rn';
const VERSIONS_FILE = OUT_DIR + '/versions.json';
const MD_FILE = OUT_DIR + '/all-releases-v2.md';
if (!fs.existsSync(OUT_DIR)) fs.mkdirSync(OUT_DIR, { recursive: true });
const CHROME_ARGS = ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--disable-blink-features=AutomationControlled'];
const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36';
const EXTRA_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br'
};
const SUB_PAGE_PATTERNS = [
{ regex: /features[/-]\d+\.htm/i, type: 'features' },
{ regex: /resolved-issues[/-]\d+\.htm/i, type: 'resolved' },
{ regex: /known-issues[/-]\d+\.htm/i, type: 'known' },
{ regex: /regulatory[/-]\d+\.htm/i, type: 'regulatory' },
];
async function setupPage(browser) {
const page = await browser.newPage();
await page.setUserAgent(USER_AGENT);
await page.setExtraHTTPHeaders(EXTRA_HEADERS);
// Skip script/style in extraction
await page.evaluateOnNewDocument(() => {
window.MutationObserver = window.MutationObserver || undefined;
});
return page;
}
// Clean DOM extraction: skip script/style blocks
async function extractPage(page, href) {
await page.goto(href, { waitUntil: 'networkidle2', timeout: 30000 });
await new Promise(r => setTimeout(r, 600));
return await page.evaluate(() => {
// Remove script + style + nav elements from clone
const document2 = document.cloneNode(true);
document2.querySelectorAll('script, style, nav, .MCWebHelpFramesetLink, .toolbar, .navigation').forEach(el => el.remove());
const mainSel = ['article', '.content', '.main-content', '.doc-content', 'main', '#content', '.topic-content', '.body-container'];
let main = document2.body;
for (const sel of mainSel) {
const el = document2.querySelector(sel);
if (el) { main = el; break; }
}
const h1 = main.querySelector('h1, h2');
const title = h1 ? h1.textContent.trim() : document2.title.trim();
const blocks = [];
main.querySelectorAll('h1, h2, h3, h4, p, li, pre, code, table').forEach(el => {
const txt = el.textContent.trim();
if (!txt || txt.length < 3) return;
const tag = el.tagName;
if (tag.match(/^H[1-4]$/)) {
blocks.push('\n' + '#'.repeat(parseInt(tag[1]) + 1) + ' ' + txt + '\n');
} else if (tag === 'LI') {
blocks.push('- ' + txt);
} else if (tag === 'PRE' || tag === 'CODE') {
blocks.push('\n```\n' + txt + '\n```\n');
} else if (tag === 'TABLE') {
const rows = [];
el.querySelectorAll('tr').forEach(tr => {
const cells = Array.from(tr.querySelectorAll('th, td')).map(c => c.textContent.trim()).join(' | ');
if (cells) rows.push('| ' + cells + ' |');
});
if (rows.length) blocks.push('\n' + rows.join('\n') + '\n');
} else {
blocks.push(txt);
}
});
return { title, body: blocks.join('\n\n') };
});
}
async function discoverVersions(page) {
if (fs.existsSync(VERSIONS_FILE)) {
const versions = JSON.parse(fs.readFileSync(VERSIONS_FILE, 'utf8'));
console.log(`📋 ${versions.length} versions (cached)`);
return versions;
}
await page.goto(BASE, { waitUntil: 'networkidle2', timeout: 60000 });
await new Promise(r => setTimeout(r, 3000));
const baseUrl = new URL(BASE);
const articleDir = baseUrl.pathname.substring(0, baseUrl.pathname.lastIndexOf('/') + 1);
const allLinks = await page.evaluate(() => {
const links = [];
document.querySelectorAll('a[href]').forEach(a => {
const text = a.textContent.trim();
if (text.length > 2) links.push({ href: a.href, text: text.slice(0, 200) });
});
return links;
});
const seen = new Set();
const versions = [];
for (const link of allLinks) {
try {
const u = new URL(link.href);
if (u.hostname !== baseUrl.hostname) continue;
if (!u.pathname.startsWith(articleDir)) continue;
if (u.hash) continue;
if (/javascript:|^#|^\?/i.test(link.href)) continue;
const fn = u.pathname.split('/').pop();
if (!fn || fn.length < 3) continue;
if (!seen.has(fn)) {
seen.add(fn);
versions.push({ href: link.href, text: link.text });
}
} catch (e) {}
}
fs.writeFileSync(VERSIONS_FILE, JSON.stringify(versions, null, 2));
console.log(`📋 ${versions.length} version pages found`);
return versions;
}
async function discoverSubPages(page, versionUrl) {
await page.goto(versionUrl, { waitUntil: 'networkidle2', timeout: 30000 });
await new Promise(r => setTimeout(r, 1500));
return await page.evaluate(() => {
const links = [];
document.querySelectorAll('a[href]').forEach(a => {
const href = a.href;
const text = a.textContent.trim();
if (/features|resolved-issues|known-issues|regulatory|whats-new/.test(href) && !/^#/.test(href) && !/Send Feedback/.test(text)) {
links.push({ href, text: text.slice(0, 80) });
}
});
return links;
});
}
async function crawl() {
const browser = await puppeteer.launch({
executablePath: CHROME_PATH,
headless: true,
args: CHROME_ARGS
});
const page = await setupPage(browser);
const versions = await discoverVersions(page);
console.log(`\n📥 Phase 1 (${versions.length} version overviews)...\n`);
fs.writeFileSync(MD_FILE, `# HPE Aruba Networking — AOS 10.x Release Notes (v2)\n\n> Source: ${BASE}\n> Versions: ${versions.length}\n> Crawled: ${new Date().toISOString()}\n\n---\n\n`);
let ok = 0, fail = 0;
const allSubPages = [];
for (let i = 0; i < versions.length; i++) {
const { href, text } = versions[i];
try {
const content = await extractPage(page, href);
const md = `## ${i + 1}. ${content.title}\n\n_Source: ${href}_\n\n${content.body || '_no content_'}\n\n---\n\n`;
fs.appendFileSync(MD_FILE, md);
ok++;
console.log(`✅ [${i+1}/${versions.length}] ${content.title.slice(0, 50).replace(/\n/g, ' ')}`);
// Phase 2: discover sub-pages for this version
const subPages = await discoverSubPages(page, href);
for (const sub of subPages) {
// Dedup: only add sub-pages for this specific version
if (sub.href.includes(href.split('/').slice(0, -1).join('/')) || sub.href.includes('/10.')) {
allSubPages.push({ parent: text, parentHref: href, ...sub });
}
}
} catch (e) {
fail++;
const md = `## ${i + 1}. ${text}\n\n_❌ Error: ${e.message}_\n\n_Source: ${href}_\n\n---\n\n`;
fs.appendFileSync(MD_FILE, md);
console.log(`❌ [${i+1}/${versions.length}] ${e.message.slice(0, 80)}`);
}
}
// Dedupe sub-pages
const seenSub = new Set();
const uniqueSub = allSubPages.filter(s => {
if (seenSub.has(s.href)) return false;
seenSub.add(s.href);
return true;
});
console.log(`\n📥 Phase 2 (${uniqueSub.length} sub-pages: features/resolved/known)...\n`);
fs.appendFileSync(MD_FILE, `\n\n# === Detail Sub-Pages (Features / Resolved Issues / Known Issues) ===\n\n`);
let subOk = 0, subFail = 0;
for (let i = 0; i < uniqueSub.length; i++) {
const sub = uniqueSub[i];
const type = SUB_PAGE_PATTERNS.find(p => p.regex.test(sub.href))?.type || 'detail';
try {
const content = await extractPage(page, sub.href);
const md = `## ${sub.parent}${type.toUpperCase()}\n\n_${sub.text}_\n\n_Source: ${sub.href}_\n\n${content.body || '_no content_'}\n\n---\n\n`;
fs.appendFileSync(MD_FILE, md);
subOk++;
console.log(`✅ [${i+1}/${uniqueSub.length}] ${type.padEnd(10)} ${sub.parent}${sub.text.slice(0, 40)}`);
} catch (e) {
subFail++;
const md = `## ${sub.parent}${type}\n\n_❌ Error: ${e.message}_\n\n_Source: ${sub.href}_\n\n---\n\n`;
fs.appendFileSync(MD_FILE, md);
console.log(`❌ [${i+1}/${uniqueSub.length}] ${type} ${e.message.slice(0, 60)}`);
}
}
await browser.close();
console.log(`\n📊 Done:`);
console.log(` Phase 1 (overviews): ${ok} OK, ${fail} failed`);
console.log(` Phase 2 (sub-pages): ${subOk} OK, ${subFail} failed`);
console.log(`📁 Output: ${MD_FILE}`);
}
crawl().catch(err => { console.error(err); process.exit(1); });
+242
View File
@@ -0,0 +1,242 @@
[
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/Home.htm",
"text": "Home"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/common%20files/topic_files/whats_new.htm",
"text": "What's New"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/all-releases.htm",
"text": "All Releases"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/common%20files/topic_files/terminology-change.htm",
"text": "Terminology Change"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.3.1/overview-1031x.htm",
"text": "10.3.1.x"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4/overview-1040x.htm",
"text": "10.4.0.x"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4.1/overview-1041x.htm",
"text": "10.4.1.x"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.5/overview-1050x.htm",
"text": "10.5.0.x"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.5.1/overview-1051x.htm",
"text": "10.5.1.x"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.6/overview-1060x.htm",
"text": "10.6.0.x"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.7/overview-1070x.htm",
"text": "10.7.0.x"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.7.1/overview-1071x.htm",
"text": "10.7.1.x"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.7.2/overview-1072x.htm",
"text": "10.7.2.x"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.8/overview-108x.htm",
"text": "10.8.x.x"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.8/10/overview-10810.htm",
"text": "10.8.1.0"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.8/02/overview-10802.htm",
"text": "10.8.0.2"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.8/01/overview-10801.htm",
"text": "10.8.0.1"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.8/00/overview-10800.htm",
"text": "10.8.0.0"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.7.2/6/overview-10726.htm",
"text": "10.7.2.6"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.7.2/5/overview-10725.htm",
"text": "10.7.2.5"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.7.2/4/overview-10724.htm",
"text": "10.7.2.4"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.7.2/3/overview-10723.htm",
"text": "10.7.2.3"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.7.2/2/overview-10722.htm",
"text": "10.7.2.2"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.7.2/1/overview-10721.htm",
"text": "10.7.2.1"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.7.2/0/overview-10720.htm",
"text": "10.7.2.0"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.7.1/1/overview-10711.htm",
"text": "10.7.1.1"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.7.1/0/overview-10710.htm",
"text": "10.7.1.0"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.7/02/overview-10702.htm",
"text": "10.7.0.2"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.7/01/overview-10701.htm",
"text": "10.7.0.1"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.7/00/overview-10700.htm",
"text": "10.7.0.0"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.6/03/overview-10603.htm",
"text": "10.6.0.3"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.6/02/overview-10602.htm",
"text": "10.6.0.2"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.6/01/overview-10601.htm",
"text": "10.6.0.1"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.6/00/overview-10600.htm",
"text": "10.6.0.0"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.5.1/1/overview-10511.htm",
"text": "10.5.1.1"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.5.1/0/overview-10510.htm",
"text": "10.5.1.0"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.5/01/overview-10501.htm",
"text": "10.5.0.1"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.5/00/overview-10500.htm",
"text": "10.5.0.0"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4.1/12/overview-104112.htm",
"text": "10.4.1.12"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4.1/11/overview-104111.htm",
"text": "10.4.1.11"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4.1/10/overview-104110.htm",
"text": "10.4.1.10"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4.1/9/overview-10419.htm",
"text": "10.4.1.9"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4.1/8/overview-10418.htm",
"text": "10.4.1.8"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4.1/7/overview-10417.htm",
"text": "10.4.1.7"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4.1/6/overview-10416.htm",
"text": "10.4.1.6"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4.1/5/overview-10415.htm",
"text": "10.4.1.5"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4.1/4/overview-10414.htm",
"text": "10.4.1.4"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4.1/3/overview-10413.htm",
"text": "10.4.1.3"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4.1/2/overview-10412.htm",
"text": "10.4.1.2"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4.1/1/overview-10411.htm",
"text": "10.4.1.1"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4.1/0/overview-10410.htm",
"text": "10.4.1.0"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4/03/overview-10403.htm",
"text": "10.4.0.3"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4/02/overview-10402.htm",
"text": "10.4.0.2"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4/01/overview-10401.htm",
"text": "10.4.0.1"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.4/00/overview-10400.htm",
"text": "10.4.0.0"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.3.1/4/overview-10314.htm",
"text": "10.3.1.4"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.3.1/3/overview-10313.htm",
"text": "10.3.1.3"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.3.1/2/overview-10312.htm",
"text": "10.3.1.2"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.3.1/1/overview-10311.htm",
"text": "10.3.1.1"
},
{
"href": "https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/10.3.1/0/overview-10310.htm",
"text": "10.3.1.0"
}
]
+142
View File
@@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>URL-to-PDF · Doc Crawler</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,system-ui,sans-serif;background:#0a0a0f;color:#f0f0f4;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}
.card{background:#131317;border:1px solid #2a2a33;border-radius:16px;padding:32px;max-width:720px;width:100%}
h1{font-size:1.5rem;font-weight:700;margin-bottom:6px;letter-spacing:-0.3px}
.sub{color:#88889a;font-size:0.85rem;margin-bottom:24px}
.tag{display:inline-block;padding:3px 10px;border-radius:6px;background:rgba(139,92,246,0.15);color:#a78bfa;font-size:0.7rem;font-weight:600;margin-right:6px;margin-bottom:8px}
.row{display:flex;gap:8px;margin-bottom:12px}
input{flex:1;padding:12px 16px;border-radius:10px;border:1px solid #2a2a33;background:#0a0a0f;color:#f0f0f4;font-size:0.9rem;font-family:inherit;transition:.2s}
input:focus{outline:none;border-color:#8b5cf6;box-shadow:0 0 0 3px rgba(139,92,246,0.1)}
button{padding:12px 20px;border-radius:10px;border:none;background:#8b5cf6;color:#fff;font-size:0.9rem;font-weight:600;cursor:pointer;font-family:inherit;transition:.2s;white-space:nowrap}
button:hover{background:#7c3aed}
button:disabled{opacity:0.4;cursor:not-allowed}
.examples{background:#0a0a0f;border:1px solid #2a2a33;border-radius:10px;padding:12px;margin-bottom:16px;font-size:0.8rem}
.examples h3{font-size:0.75rem;color:#88889a;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.5px}
.examples a{color:#a78bfa;text-decoration:none;display:block;padding:4px 0;font-family:monospace;font-size:0.75rem}
.examples a:hover{color:#8b5cf6}
.status{margin-top:16px;padding:14px 16px;border-radius:10px;font-size:0.85rem;display:none}
.status.active{display:block}
.status.info{background:rgba(96,165,250,0.1);color:#60a5fa;border:1px solid rgba(96,165,250,0.3)}
.status.success{background:rgba(16,185,129,0.1);color:#34d399;border:1px solid rgba(16,185,129,0.3)}
.status.error{background:rgba(239,68,68,0.1);color:#f87171;border:1px solid rgba(239,68,68,0.3)}
.progress{height:4px;background:#1a1a22;border-radius:2px;margin-top:8px;overflow:hidden}
.progress-bar{height:100%;background:#8b5cf6;border-radius:2px;transition:width .3s;width:0}
.result{margin-top:12px;font-size:0.8rem}
.result a{color:#34d399;text-decoration:none;font-weight:600}
.result a:hover{text-decoration:underline}
.info-box{background:#0a0a0f;border:1px solid #2a2a33;border-radius:10px;padding:14px;margin-bottom:16px;font-size:0.78rem;color:#bcbcc8;line-height:1.6}
.info-box code{background:#1a1a22;padding:2px 6px;border-radius:4px;font-size:0.72rem;color:#a78bfa}
</style>
</head>
<body>
<div class="card">
<h1>📄 URL-to-PDF · Doc Crawler</h1>
<p class="sub">Paste a documentation URL → get all sub-pages as Markdown</p>
<div class="info-box">
🐕 <strong>IT狗 2026-07-24</strong> — Two-phase crawler: discovers version pages, then fetches each version's <em>features / resolved / known issues / regulatory</em> sub-pages. Currently tested with HPE AOS 10.x (209 pages).
</div>
<span class="tag">v2</span><span class="tag">Two-phase</span><span class="tag">Markdown</span>
<div class="row">
<input id="urlInput" type="url" placeholder="https://example.com/docs/all-releases.htm" value="https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/all-releases.htm">
<button id="goBtn" onclick="startCrawl()">Crawl</button>
</div>
<div class="examples">
<h3>📚 Tested / Compatible</h3>
<a href="#" onclick="event.preventDefault();document.getElementById(\"urlInput\").value=this.textContent">https://arubanetworking.hpe.com/techdocs/AOS_10.x_RN_WebHelp/Content/all-releases.htm</a>
<a href="#" onclick="event.preventDefault();document.getElementById(\"urlInput\").value=this.textContent">https://arubanetworking.hpe.com/techdocs/AOS_8.x_RN_WebHelp/Content/all-releases.htm</a>
</div>
<div id="status" class="status"></div>
</div>
<script>
const API = "/url2pdf";
const input = document.getElementById("urlInput");
const btn = document.getElementById("goBtn");
const status = document.getElementById("status");
async function startCrawl() {
const url = (input.value || "").trim();
if (!url) { showStatus("error", "Please enter a URL"); return; }
if (!/^https?:\/\//.test(url)) { showStatus("error", "URL must start with http:// or https://"); return; }
btn.disabled = true;
showStatus("info", "🚀 Starting crawl...");
try {
const r = await fetch(API + "/crawl", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({url})
});
const data = await r.json();
if (data.error) throw new Error(data.error);
const job = data.job;
showStatus("info", `⏳ Crawl started (job: <code>${job}</code>). Polling status...`);
// Poll status
const startTime = Date.now();
let pollCount = 0;
while (true) {
await new Promise(r => setTimeout(r, 5000));
pollCount++;
const elapsed = Math.round((Date.now() - startTime) / 1000);
const sr = await fetch(API + "/crawl/status?job=" + job);
const sd = await sr.json();
const statusText = sd.status || "pending";
const sizeMB = sd.output_size ? (sd.output_size / 1024 / 1024).toFixed(1) + " MB" : "";
if (statusText === "done") {
showStatus("success", `✅ Done in ${elapsed}s — ${sizeMB} of markdown ready`, job);
btn.disabled = false;
return;
} else if (statusText.startsWith("failed")) {
showStatus("error", `❌ Failed: ${statusText}<br><pre style="white-space:pre-wrap;font-size:0.7rem;color:#f87171;margin-top:8px;max-height:200px;overflow:auto">${sd.log_tail || ""}</pre>`);
btn.disabled = false;
return;
} else if (statusText.startsWith("done:")) {
// legacy format
showStatus("success", `${statusText} (${elapsed}s)`, job);
btn.disabled = false;
return;
} else {
showStatus("info", `${statusText} (${elapsed}s elapsed, poll #${pollCount})`);
}
if (elapsed > 1200) { // 20min timeout
showStatus("error", "⏱️ Timeout (>20min). Check job manually.");
btn.disabled = false;
return;
}
}
} catch (e) {
showStatus("error", "❌ Error: " + e.message);
btn.disabled = false;
}
}
function showStatus(type, html, job) {
status.className = "status active " + type;
const jobLink = job ? `<div class="result">📥 <a href="${API}/crawl/download?job=${job}&format=md" download>Download output.md</a></div>` : "";
status.innerHTML = html + jobLink;
}
// Allow Enter key
input.addEventListener("keypress", e => { if (e.key === "Enter") startCrawl(); });
</script>
</body>
</html>
+61 -11
View File
@@ -313,6 +313,43 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.write(json.dumps(data).encode()) self.wfile.write(json.dumps(data).encode())
def do_GET(self): def do_GET(self):
# Static file serving: <file> or /url2pdf-tools/<file> → /opt/url2pdf/<file>
# (nginx strips /url2pdf prefix; we accept both with and without it)
# Static file serving: <file> or /url2pdf-tools/<file> -> /opt/url2pdf/<file>
# (nginx strips /url2pdf prefix; we accept both with and without it)
if self.path.startswith("/url2pdf-tools/"):
rel = self.path[len("/url2pdf-tools/"):]
elif "." in self.path.split("?")[0].split("/")[-1]:
rel = self.path.lstrip("/")
else:
rel = None
# Always serve index.html for root paths or empty
if rel is None or rel == "" or rel == "/":
rel = "index.html"
# Prevent path traversal
safe = os.path.normpath(rel).lstrip("/")
if safe.startswith("..") or "/.." in safe:
self.send_error(400, "Bad path")
return
fp = os.path.join("/opt/url2pdf", safe)
fp = os.path.join("/opt/url2pdf", safe)
if os.path.isfile(fp):
import mimetypes
ct, _ = mimetypes.guess_type(fp)
if ct is None:
ct = "application/octet-stream"
with open(fp, "rb") as f:
data = f.read()
self.send_response(200)
self.send_header("Content-Type", ct)
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
return
# file not found under /opt/url2pdf - fall through to API or HTML fallback
self.send_error(404, "Not found: " + fp)
return
if self.path.startswith("/crawl/status"): if self.path.startswith("/crawl/status"):
qs = urllib.parse.urlparse(self.path).query qs = urllib.parse.urlparse(self.path).query
params = urllib.parse.parse_qs(qs) params = urllib.parse.parse_qs(qs)
@@ -328,6 +365,7 @@ class Handler(BaseHTTPRequestHandler):
self._json(200, {"status": "pending"}) self._json(200, {"status": "pending"})
return return
if self.path.startswith("/crawl/download"): if self.path.startswith("/crawl/download"):
qs = urllib.parse.urlparse(self.path).query qs = urllib.parse.urlparse(self.path).query
params = urllib.parse.parse_qs(qs) params = urllib.parse.parse_qs(qs)
@@ -424,30 +462,42 @@ class Handler(BaseHTTPRequestHandler):
jdir = os.path.join(JOBS_DIR, job) jdir = os.path.join(JOBS_DIR, job)
os.makedirs(jdir, exist_ok=True) os.makedirs(jdir, exist_ok=True)
# Write crawler script to npm dir so require() works # Use v2 crawler (two-phase: discover versions + sub-pages)
crawl_js = os.path.join(CWD, f"crawler_{job}.js") v2_crawler = os.path.join(CWD, "crawl_hpe_v2.js")
with open(crawl_js, "w") as f: if not os.path.exists(v2_crawler):
f.write(CRAWLER_JS) self._json(500, {"error": "v2 crawler not installed at /opt/url2pdf/crawl_hpe_v2.js"})
return
# Write initial status # Write initial status
with open(os.path.join(jdir, "status.json"), "w") as f: with open(os.path.join(jdir, "status.json"), "w") as f:
json.dump({"status": "starting", "time": time.strftime("%Y-%m-%dT%H:%M:%S")}, f) json.dump({"status": "starting", "time": time.strftime("%Y-%m-%dT%H:%M:%S")}, f)
# Start background crawler # Start background crawler (v2 writes output.md to /tmp/hpe-aruba-aos10-rn/, so copy to jdir)
def run(): def run():
subprocess.run( r = subprocess.run(
["timeout", "600", "node", crawl_js, url, jdir], ["timeout", "900", "node", v2_crawler, url],
capture_output=True, text=True, cwd=CWD, capture_output=True, text=True, cwd=CWD,
env={**os.environ, "NODE_PATH": os.path.join(CWD, "node_modules")} env={**os.environ, "NODE_PATH": os.path.join(CWD, "node_modules")}
) )
# Clean up script # Copy v2 output to jdir
try: os.unlink(crawl_js) import shutil
except: pass src_md = "/tmp/hpe-aruba-aos10-rn/all-releases-v2.md"
dst_md = os.path.join(jdir, "output.md")
if os.path.exists(src_md):
shutil.copy(src_md, dst_md)
# Update status
with open(os.path.join(jdir, "status.json"), "w") as f:
json.dump({
"status": "done" if r.returncode == 0 else f"failed:{r.returncode}",
"time": time.strftime("%Y-%m-%dT%H:%M:%S"),
"output_size": os.path.getsize(dst_md) if os.path.exists(dst_md) else 0,
"log_tail": r.stdout[-500:] if r.stdout else r.stderr[-500:]
}, f)
t = threading.Thread(target=run, daemon=True) t = threading.Thread(target=run, daemon=True)
t.start() t.start()
self._json(200, {"job": job, "status": "started"}) self._json(200, {"job": job, "status": "started", "crawler": "v2"})
else: else:
self.send_error(404) self.send_error(404)