doc2vec 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/content-processor.js +154 -93
- package/dist/database.js +4 -4
- package/dist/doc2vec.js +3 -3
- package/package.json +1 -1
|
@@ -247,83 +247,121 @@ class ContentProcessor {
|
|
|
247
247
|
let pdfProcessedCount = 0;
|
|
248
248
|
let errorCount = 0;
|
|
249
249
|
let hasNetworkErrors = false;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
250
|
+
// Launch a single browser instance and reuse one page (tab) for all URLs
|
|
251
|
+
let browser = null;
|
|
252
|
+
let page = null;
|
|
253
|
+
const launchBrowser = async () => {
|
|
254
|
+
let executablePath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
255
|
+
if (!executablePath) {
|
|
256
|
+
if (fs.existsSync('/usr/bin/chromium')) {
|
|
257
|
+
executablePath = '/usr/bin/chromium';
|
|
258
|
+
}
|
|
259
|
+
else if (fs.existsSync('/usr/bin/chromium-browser')) {
|
|
260
|
+
executablePath = '/usr/bin/chromium-browser';
|
|
261
|
+
}
|
|
262
262
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
263
|
+
const b = await puppeteer_1.default.launch({
|
|
264
|
+
executablePath,
|
|
265
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
266
|
+
});
|
|
267
|
+
const p = await b.newPage();
|
|
268
|
+
return { browser: b, page: p };
|
|
269
|
+
};
|
|
270
|
+
const ensureBrowser = async () => {
|
|
271
|
+
if (!browser || !browser.isConnected()) {
|
|
272
|
+
logger.info(browser ? 'Browser disconnected, relaunching...' : 'Launching browser...');
|
|
273
|
+
const launched = await launchBrowser();
|
|
274
|
+
browser = launched.browser;
|
|
275
|
+
page = launched.page;
|
|
276
|
+
}
|
|
277
|
+
return page;
|
|
278
|
+
};
|
|
279
|
+
try {
|
|
280
|
+
while (queue.length > 0) {
|
|
281
|
+
const url = queue.shift();
|
|
282
|
+
if (!url)
|
|
283
|
+
continue;
|
|
284
|
+
const normalizedUrl = utils_1.Utils.normalizeUrl(url);
|
|
285
|
+
if (visitedUrls.has(normalizedUrl))
|
|
286
|
+
continue;
|
|
287
|
+
visitedUrls.add(normalizedUrl);
|
|
288
|
+
if (!utils_1.Utils.shouldProcessUrl(url)) {
|
|
289
|
+
logger.debug(`Skipping URL with unsupported extension: ${url}`);
|
|
290
|
+
skippedCount++;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
logger.info(`Crawling: ${url}`);
|
|
295
|
+
const sources = referrers.get(url) ?? new Set([baseUrl]);
|
|
296
|
+
// For HTML pages, ensure the browser is running and pass the shared page
|
|
297
|
+
// For PDFs, processPage handles them without Puppeteer
|
|
298
|
+
const currentPage = utils_1.Utils.isPdfUrl(url) ? undefined : await ensureBrowser();
|
|
299
|
+
const result = await this.processPage(url, sourceConfig, (reportedUrl, status) => {
|
|
300
|
+
if (status === 404) {
|
|
301
|
+
for (const source of sources) {
|
|
302
|
+
addBrokenLink(source, reportedUrl);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}, currentPage);
|
|
306
|
+
if (result.content !== null) {
|
|
307
|
+
await processPageContent(url, result.content);
|
|
308
|
+
if (utils_1.Utils.isPdfUrl(url)) {
|
|
309
|
+
pdfProcessedCount++;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
processedCount++;
|
|
270
313
|
}
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
if (content !== null) {
|
|
274
|
-
await processPageContent(url, content);
|
|
275
|
-
if (utils_1.Utils.isPdfUrl(url)) {
|
|
276
|
-
pdfProcessedCount++;
|
|
277
314
|
}
|
|
278
315
|
else {
|
|
279
|
-
|
|
316
|
+
skippedSizeCount++;
|
|
280
317
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const fullUrl = utils_1.Utils.buildUrl(href, pageUrlForLinks);
|
|
297
|
-
if (fullUrl.startsWith(sourceConfig.url)) {
|
|
298
|
-
addReferrer(fullUrl, pageUrlForLinks);
|
|
299
|
-
if (!visitedUrls.has(utils_1.Utils.normalizeUrl(fullUrl))) {
|
|
300
|
-
if (!queue.includes(fullUrl)) {
|
|
301
|
-
queue.push(fullUrl);
|
|
302
|
-
newLinksFound++;
|
|
318
|
+
// Use links extracted from the full rendered DOM by processPage
|
|
319
|
+
// (no separate axios request needed)
|
|
320
|
+
if (result.links.length > 0) {
|
|
321
|
+
const pageUrlForLinks = result.finalUrl || url;
|
|
322
|
+
logger.debug(`Finding links on page ${url}`);
|
|
323
|
+
let newLinksFound = 0;
|
|
324
|
+
for (const href of result.links) {
|
|
325
|
+
const fullUrl = utils_1.Utils.buildUrl(href, pageUrlForLinks);
|
|
326
|
+
if (fullUrl.startsWith(sourceConfig.url)) {
|
|
327
|
+
addReferrer(fullUrl, pageUrlForLinks);
|
|
328
|
+
if (!visitedUrls.has(utils_1.Utils.normalizeUrl(fullUrl))) {
|
|
329
|
+
if (!queue.includes(fullUrl)) {
|
|
330
|
+
queue.push(fullUrl);
|
|
331
|
+
newLinksFound++;
|
|
332
|
+
}
|
|
303
333
|
}
|
|
304
334
|
}
|
|
305
335
|
}
|
|
306
|
-
|
|
307
|
-
logger.debug(`Found ${newLinksFound} new links on ${url}`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
catch (error) {
|
|
311
|
-
logger.error(`Failed during processing or link discovery for ${url}:`, error);
|
|
312
|
-
errorCount++;
|
|
313
|
-
const status = this.getHttpStatus(error);
|
|
314
|
-
if (status === 404) {
|
|
315
|
-
const sources = referrers.get(url) ?? new Set([baseUrl]);
|
|
316
|
-
for (const source of sources) {
|
|
317
|
-
addBrokenLink(source, url);
|
|
336
|
+
logger.debug(`Found ${newLinksFound} new links on ${url}`);
|
|
318
337
|
}
|
|
319
338
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
339
|
+
catch (error) {
|
|
340
|
+
logger.error(`Failed during processing or link discovery for ${url}:`, error);
|
|
341
|
+
errorCount++;
|
|
342
|
+
const status = this.getHttpStatus(error);
|
|
343
|
+
if (status === 404) {
|
|
344
|
+
const sources = referrers.get(url) ?? new Set([baseUrl]);
|
|
345
|
+
for (const source of sources) {
|
|
346
|
+
addBrokenLink(source, url);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Check if this is a network error (DNS resolution, connection issues, etc.)
|
|
350
|
+
if (this.isNetworkError(error)) {
|
|
351
|
+
hasNetworkErrors = true;
|
|
352
|
+
logger.warn(`Network error detected for ${url}, this may affect cleanup decisions`);
|
|
353
|
+
}
|
|
324
354
|
}
|
|
325
355
|
}
|
|
326
356
|
}
|
|
357
|
+
finally {
|
|
358
|
+
// Close the shared browser instance when the crawl is done
|
|
359
|
+
const browserToClose = browser;
|
|
360
|
+
if (browserToClose && browserToClose.isConnected()) {
|
|
361
|
+
await browserToClose.close();
|
|
362
|
+
logger.debug('Shared browser closed after crawl completed');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
327
365
|
logger.info(`Crawl completed. HTML Pages: ${processedCount}, PDFs: ${pdfProcessedCount}, Skipped (Extension): ${skippedCount}, Skipped (Size): ${skippedSizeCount}, Errors: ${errorCount}`);
|
|
328
366
|
if (hasNetworkErrors) {
|
|
329
367
|
logger.warn('Network errors were encountered during crawling. Cleanup may be skipped to avoid removing valid chunks.');
|
|
@@ -369,7 +407,7 @@ class ContentProcessor {
|
|
|
369
407
|
}
|
|
370
408
|
return false;
|
|
371
409
|
}
|
|
372
|
-
async processPage(url, sourceConfig, onHttpStatus) {
|
|
410
|
+
async processPage(url, sourceConfig, onHttpStatus, existingPage) {
|
|
373
411
|
const logger = this.logger.child('page-processor');
|
|
374
412
|
logger.debug(`Processing content from ${url}`);
|
|
375
413
|
// Check if this is a PDF URL
|
|
@@ -380,9 +418,9 @@ class ContentProcessor {
|
|
|
380
418
|
// Check size limit for PDF content
|
|
381
419
|
if (markdown.length > sourceConfig.max_size) {
|
|
382
420
|
logger.warn(`PDF content (${markdown.length} chars) exceeds max size (${sourceConfig.max_size}). Skipping ${url}.`);
|
|
383
|
-
return null;
|
|
421
|
+
return { content: null, links: [], finalUrl: url };
|
|
384
422
|
}
|
|
385
|
-
return markdown;
|
|
423
|
+
return { content: markdown, links: [], finalUrl: url };
|
|
386
424
|
}
|
|
387
425
|
catch (error) {
|
|
388
426
|
const status = this.getHttpStatus(error);
|
|
@@ -393,27 +431,35 @@ class ContentProcessor {
|
|
|
393
431
|
throw error;
|
|
394
432
|
}
|
|
395
433
|
logger.error(`Failed to process PDF ${url}:`, error);
|
|
396
|
-
return null;
|
|
434
|
+
return { content: null, links: [], finalUrl: url };
|
|
397
435
|
}
|
|
398
436
|
}
|
|
399
|
-
//
|
|
437
|
+
// HTML page processing logic
|
|
438
|
+
// If an existing page (tab) is provided, reuse it; otherwise launch a standalone browser
|
|
400
439
|
let browser = null;
|
|
440
|
+
let page;
|
|
441
|
+
const ownsTheBrowser = !existingPage;
|
|
401
442
|
try {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
443
|
+
if (existingPage) {
|
|
444
|
+
page = existingPage;
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// Standalone mode: launch a browser for this single page
|
|
448
|
+
let executablePath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
449
|
+
if (!executablePath) {
|
|
450
|
+
if (fs.existsSync('/usr/bin/chromium')) {
|
|
451
|
+
executablePath = '/usr/bin/chromium';
|
|
452
|
+
}
|
|
453
|
+
else if (fs.existsSync('/usr/bin/chromium-browser')) {
|
|
454
|
+
executablePath = '/usr/bin/chromium-browser';
|
|
455
|
+
}
|
|
410
456
|
}
|
|
457
|
+
browser = await puppeteer_1.default.launch({
|
|
458
|
+
executablePath,
|
|
459
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
460
|
+
});
|
|
461
|
+
page = await browser.newPage();
|
|
411
462
|
}
|
|
412
|
-
browser = await puppeteer_1.default.launch({
|
|
413
|
-
executablePath,
|
|
414
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
415
|
-
});
|
|
416
|
-
const page = await browser.newPage();
|
|
417
463
|
logger.debug(`Navigating to ${url}`);
|
|
418
464
|
const response = await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
|
|
419
465
|
const status = response?.status();
|
|
@@ -422,8 +468,24 @@ class ContentProcessor {
|
|
|
422
468
|
error.status = status;
|
|
423
469
|
throw error;
|
|
424
470
|
}
|
|
471
|
+
// Get the final URL after any redirects
|
|
472
|
+
const finalUrl = page.url();
|
|
473
|
+
// Extract ALL links from the full rendered DOM before any content filtering
|
|
474
|
+
// This searches the entire document, not just the main content area
|
|
475
|
+
const links = await page.evaluate(() => {
|
|
476
|
+
const anchors = document.querySelectorAll('a[href]');
|
|
477
|
+
const hrefs = [];
|
|
478
|
+
anchors.forEach(a => {
|
|
479
|
+
const href = a.getAttribute('href');
|
|
480
|
+
if (href && !href.startsWith('#') && !href.startsWith('mailto:')) {
|
|
481
|
+
hrefs.push(href);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
return hrefs;
|
|
485
|
+
});
|
|
486
|
+
logger.debug(`Extracted ${links.length} links from full DOM of ${url}`);
|
|
425
487
|
const htmlContent = await page.evaluate(() => {
|
|
426
|
-
//
|
|
488
|
+
// Try specific content selectors first, then fall back to broader ones
|
|
427
489
|
const mainContentElement = document.querySelector('.docs-content') || // Common docs pattern
|
|
428
490
|
document.querySelector('.doc-content') || // Alternative docs pattern
|
|
429
491
|
document.querySelector('.markdown-body') || // GitHub-style
|
|
@@ -435,8 +497,7 @@ class ContentProcessor {
|
|
|
435
497
|
});
|
|
436
498
|
if (htmlContent.length > sourceConfig.max_size) {
|
|
437
499
|
logger.warn(`Raw HTML content (${htmlContent.length} chars) exceeds max size (${sourceConfig.max_size}). Skipping detailed processing for ${url}.`);
|
|
438
|
-
|
|
439
|
-
return null;
|
|
500
|
+
return { content: null, links, finalUrl };
|
|
440
501
|
}
|
|
441
502
|
logger.debug(`Got HTML content (${htmlContent.length} chars), creating DOM`);
|
|
442
503
|
const dom = new jsdom_1.JSDOM(htmlContent);
|
|
@@ -446,7 +507,7 @@ class ContentProcessor {
|
|
|
446
507
|
pre.setAttribute('data-readable-content-score', '100');
|
|
447
508
|
this.markCodeParents(pre.parentElement);
|
|
448
509
|
});
|
|
449
|
-
//
|
|
510
|
+
// Extract H1s BEFORE Readability - it often strips them as "chrome"
|
|
450
511
|
// We'll inject them back after Readability processing
|
|
451
512
|
const h1Elements = document.querySelectorAll('h1');
|
|
452
513
|
const extractedH1s = [];
|
|
@@ -468,8 +529,7 @@ class ContentProcessor {
|
|
|
468
529
|
const article = reader.parse();
|
|
469
530
|
if (!article) {
|
|
470
531
|
logger.warn(`Failed to parse article content with Readability for ${url}`);
|
|
471
|
-
|
|
472
|
-
return null;
|
|
532
|
+
return { content: null, links, finalUrl };
|
|
473
533
|
}
|
|
474
534
|
// Debug: Log what Readability extracted
|
|
475
535
|
logger.debug(`[Readability Debug] article.title: "${article.title}"`);
|
|
@@ -478,7 +538,7 @@ class ContentProcessor {
|
|
|
478
538
|
logger.debug(`[Readability Debug] Contains H1 tag: ${article.content?.includes('<h1')}`);
|
|
479
539
|
logger.debug(`[Readability Debug] Contains H2 tag: ${article.content?.includes('<h2')}`);
|
|
480
540
|
logger.debug(`[Readability Debug] Contains original-h1 class: ${article.content?.includes('original-h1')}`);
|
|
481
|
-
//
|
|
541
|
+
// Restore H1s: find elements with our marker class and convert back from H2
|
|
482
542
|
const articleDom = new jsdom_1.JSDOM(article.content);
|
|
483
543
|
const articleDoc = articleDom.window.document;
|
|
484
544
|
const originalH1Elements = articleDoc.querySelectorAll('.original-h1');
|
|
@@ -515,7 +575,7 @@ class ContentProcessor {
|
|
|
515
575
|
});
|
|
516
576
|
logger.debug(`Converting HTML to Markdown`);
|
|
517
577
|
let markdown = this.turndownService.turndown(cleanHtml);
|
|
518
|
-
//
|
|
578
|
+
// Inject extracted H1s back if they're not in the markdown
|
|
519
579
|
// Readability often strips them as "page chrome"
|
|
520
580
|
// Use article.title as fallback if no H1 was extracted
|
|
521
581
|
const pageTitle = extractedH1s.length > 0 ? extractedH1s[0] : (article.title?.trim() || '');
|
|
@@ -535,7 +595,7 @@ class ContentProcessor {
|
|
|
535
595
|
}
|
|
536
596
|
}
|
|
537
597
|
logger.debug(`Markdown conversion complete (${markdown.length} chars)`);
|
|
538
|
-
return markdown;
|
|
598
|
+
return { content: markdown, links, finalUrl };
|
|
539
599
|
}
|
|
540
600
|
catch (error) {
|
|
541
601
|
const status = this.getHttpStatus(error);
|
|
@@ -546,10 +606,11 @@ class ContentProcessor {
|
|
|
546
606
|
throw error;
|
|
547
607
|
}
|
|
548
608
|
logger.error(`Error processing page ${url}:`, error);
|
|
549
|
-
return null;
|
|
609
|
+
return { content: null, links: [], finalUrl: url };
|
|
550
610
|
}
|
|
551
611
|
finally {
|
|
552
|
-
|
|
612
|
+
// Only close the browser if we launched it ourselves (standalone mode)
|
|
613
|
+
if (ownsTheBrowser && browser && browser.isConnected()) {
|
|
553
614
|
await browser.close();
|
|
554
615
|
logger.debug(`Browser closed for ${url}`);
|
|
555
616
|
}
|
package/dist/database.js
CHANGED
|
@@ -374,10 +374,10 @@ class DatabaseManager {
|
|
|
374
374
|
chunk.metadata.version
|
|
375
375
|
];
|
|
376
376
|
if (hasBranchColumn) {
|
|
377
|
-
insertValues.push(chunk.metadata.branch ??
|
|
377
|
+
insertValues.push(chunk.metadata.branch ?? '');
|
|
378
378
|
}
|
|
379
379
|
if (hasRepoColumn) {
|
|
380
|
-
insertValues.push(chunk.metadata.repo ??
|
|
380
|
+
insertValues.push(chunk.metadata.repo ?? '');
|
|
381
381
|
}
|
|
382
382
|
insertValues.push(JSON.stringify(chunk.metadata.heading_hierarchy), chunk.metadata.section, chunk.metadata.chunk_id, chunk.content, chunk.metadata.url, hash, chunkIndex, totalChunks);
|
|
383
383
|
insertStmt.run(...insertValues);
|
|
@@ -389,10 +389,10 @@ class DatabaseManager {
|
|
|
389
389
|
chunk.metadata.version
|
|
390
390
|
];
|
|
391
391
|
if (hasBranchColumn) {
|
|
392
|
-
updateValues.push(chunk.metadata.branch ??
|
|
392
|
+
updateValues.push(chunk.metadata.branch ?? '');
|
|
393
393
|
}
|
|
394
394
|
if (hasRepoColumn) {
|
|
395
|
-
updateValues.push(chunk.metadata.repo ??
|
|
395
|
+
updateValues.push(chunk.metadata.repo ?? '');
|
|
396
396
|
}
|
|
397
397
|
updateValues.push(JSON.stringify(chunk.metadata.heading_hierarchy), chunk.metadata.section, chunk.content, chunk.metadata.url, hash, chunkIndex, totalChunks, chunk.metadata.chunk_id);
|
|
398
398
|
updateStmt.run(...updateValues);
|
package/dist/doc2vec.js
CHANGED
|
@@ -402,7 +402,7 @@ class Doc2Vec {
|
|
|
402
402
|
const chunks = await this.contentProcessor.chunkMarkdown(content, config, url);
|
|
403
403
|
logger.info(`Created ${chunks.length} chunks`);
|
|
404
404
|
if (chunks.length > 0) {
|
|
405
|
-
const chunkProgress = logger.progress(`
|
|
405
|
+
const chunkProgress = logger.progress(`Processing chunks for ${url}`, chunks.length);
|
|
406
406
|
for (let i = 0; i < chunks.length; i++) {
|
|
407
407
|
const chunk = chunks[i];
|
|
408
408
|
validChunkIds.add(chunk.metadata.chunk_id);
|
|
@@ -561,7 +561,7 @@ class Doc2Vec {
|
|
|
561
561
|
const chunks = await this.contentProcessor.chunkMarkdown(content, config, fileUrl);
|
|
562
562
|
logger.info(`Created ${chunks.length} chunks`);
|
|
563
563
|
if (chunks.length > 0) {
|
|
564
|
-
const chunkProgress = logger.progress(`
|
|
564
|
+
const chunkProgress = logger.progress(`Processing chunks for ${filePath}`, chunks.length);
|
|
565
565
|
for (let i = 0; i < chunks.length; i++) {
|
|
566
566
|
const chunk = chunks[i];
|
|
567
567
|
validChunkIds.add(chunk.metadata.chunk_id);
|
|
@@ -729,7 +729,7 @@ class Doc2Vec {
|
|
|
729
729
|
const chunks = await this.contentProcessor.chunkCode(content, config, fileUrl, relativePath || filePath, repoBranch || config.branch, config.repo);
|
|
730
730
|
logger.info(`Created ${chunks.length} chunks`);
|
|
731
731
|
if (chunks.length > 0) {
|
|
732
|
-
const chunkProgress = logger.progress(`
|
|
732
|
+
const chunkProgress = logger.progress(`Processing chunks for ${relativePath || filePath}`, chunks.length);
|
|
733
733
|
for (let i = 0; i < chunks.length; i++) {
|
|
734
734
|
const chunk = chunks[i];
|
|
735
735
|
validChunkIds.add(chunk.metadata.chunk_id);
|