@llm-newsletter-kit/core 1.1.4 → 1.1.6
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/index.cjs +134 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +134 -50
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -838,8 +838,14 @@ class AnalysisChain extends Chain {
|
|
|
838
838
|
}
|
|
839
839
|
}
|
|
840
840
|
|
|
841
|
+
function preprocessBoldSyntax(markdown) {
|
|
842
|
+
// Convert **text** to <strong>text</strong> before marked parsing
|
|
843
|
+
// This fixes issues where marked doesn't properly handle bold syntax with parentheses
|
|
844
|
+
return markdown.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
845
|
+
}
|
|
841
846
|
function markdownToHtml(markdown) {
|
|
842
|
-
const
|
|
847
|
+
const preprocessed = preprocessBoldSyntax(markdown);
|
|
848
|
+
const html = marked.marked.parse(preprocessed);
|
|
843
849
|
const window = new jsdom.JSDOM('').window;
|
|
844
850
|
const purify = DOMPurify(window);
|
|
845
851
|
const sanitized = purify.sanitize(html);
|
|
@@ -849,22 +855,9 @@ function markdownToHtml(markdown) {
|
|
|
849
855
|
return correctUnconvertedBoldSyntax(withDelReplaced);
|
|
850
856
|
}
|
|
851
857
|
function addTargetBlankToAnchors(htmlString) {
|
|
852
|
-
//
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
const regex = /<a(\s+[^>]*?)?(?<!target="[^"]*")>/gi;
|
|
856
|
-
// Use regex to find '<a>' tags and add 'target="_blank"'
|
|
857
|
-
return htmlString.replace(regex, (_match, attributes) => {
|
|
858
|
-
// Handle undefined attributes as empty string
|
|
859
|
-
const currentAttributes = attributes || '';
|
|
860
|
-
// Double check if target attribute exists (safety check for regex limitations)
|
|
861
|
-
if (currentAttributes.includes('target=')) {
|
|
862
|
-
return `<a${currentAttributes}>`; // If target attribute exists, return without modification
|
|
863
|
-
}
|
|
864
|
-
else {
|
|
865
|
-
// Add target="_blank" attribute
|
|
866
|
-
return `<a${currentAttributes} target="_blank">`;
|
|
867
|
-
}
|
|
858
|
+
// DOMPurify removes target attributes, so we can safely add target="_blank" to all anchors
|
|
859
|
+
return htmlString.replace(/<a\s+([^>]*)>/gi, (_match, attributes) => {
|
|
860
|
+
return `<a ${attributes} target="_blank">`;
|
|
868
861
|
});
|
|
869
862
|
}
|
|
870
863
|
function replaceDelTagsWithTilde(htmlString) {
|
|
@@ -877,19 +870,20 @@ function correctUnconvertedBoldSyntax(htmlString) {
|
|
|
877
870
|
return htmlString.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>');
|
|
878
871
|
}
|
|
879
872
|
function correctMalformedUrls(htmlString) {
|
|
880
|
-
// Pattern matches anchors with `)` followed by URL-encoded characters
|
|
873
|
+
// Pattern matches anchors with `)` followed by optional closing markup (</b> or **) and URL-encoded characters
|
|
881
874
|
// Capture groups:
|
|
882
875
|
// 1: attributes before href
|
|
883
876
|
// 2: URL base (before `)`)
|
|
884
|
-
// 3:
|
|
885
|
-
// 4:
|
|
886
|
-
// 5:
|
|
887
|
-
// 6:
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
//
|
|
892
|
-
|
|
877
|
+
// 3: closing markup (</b> or ** or empty)
|
|
878
|
+
// 4: URL-encoded part (starts with %)
|
|
879
|
+
// 5: attributes after href
|
|
880
|
+
// 6: link text base (before `)`)
|
|
881
|
+
// 7: text after `)` in link text (may include **)
|
|
882
|
+
const regex = /<a\s+([^>]*?)href="([^"]*?)\)((?:<\/b>|\*\*)?)(%[0-9A-Fa-f]{2}[^"]*?)"([^>]*?)>([^<]*?)\)((?:\*\*)?[^<]*?)<\/a>/g;
|
|
883
|
+
return htmlString.replace(regex, (_match, beforeHref, urlBase, closingMarkup, _encodedPart, afterHref, textBase, textAfterClosingParen) => {
|
|
884
|
+
// Remove leading ** from textAfterClosingParen since it's already captured as closingMarkup (</b>)
|
|
885
|
+
const cleanedText = textAfterClosingParen.replace(/^\*\*/, '');
|
|
886
|
+
return `<a ${beforeHref}href="${urlBase}"${afterHref}>${textBase}</a>${closingMarkup})${cleanedText}`;
|
|
893
887
|
});
|
|
894
888
|
}
|
|
895
889
|
|
|
@@ -1397,6 +1391,7 @@ async function getHtmlFromUrl(logger, url, referer = 'https://www.google.com/')
|
|
|
1397
1391
|
throw lastError;
|
|
1398
1392
|
}
|
|
1399
1393
|
|
|
1394
|
+
const toErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
1400
1395
|
class CrawlingChain extends Chain {
|
|
1401
1396
|
constructor(config) {
|
|
1402
1397
|
const provider = config.provider;
|
|
@@ -1426,17 +1421,15 @@ class CrawlingChain extends Chain {
|
|
|
1426
1421
|
target: ({ target }) => target,
|
|
1427
1422
|
})
|
|
1428
1423
|
.pipe({
|
|
1429
|
-
|
|
1424
|
+
detailFetchResult: ({ target, list }) => this.fetchDetailPagesHtml(target, list),
|
|
1430
1425
|
target: ({ target }) => target,
|
|
1431
|
-
list: ({ list }) => list,
|
|
1432
1426
|
})
|
|
1433
1427
|
.pipe({
|
|
1434
|
-
|
|
1428
|
+
detailParseResult: ({ target, detailFetchResult }) => this.parseDetailPagesHtml(target, detailFetchResult.list, detailFetchResult.detailPagesHtmlWithPipelineId),
|
|
1435
1429
|
target: ({ target }) => target,
|
|
1436
|
-
list: ({ list }) => list,
|
|
1437
1430
|
})
|
|
1438
1431
|
.pipe({
|
|
1439
|
-
processedArticles: ({ target,
|
|
1432
|
+
processedArticles: ({ target, detailParseResult }) => this.mergeParsedArticles(target, detailParseResult.list, detailParseResult.parsedDetails),
|
|
1440
1433
|
target: ({ target }) => target,
|
|
1441
1434
|
})
|
|
1442
1435
|
.pipe({
|
|
@@ -1464,7 +1457,21 @@ class CrawlingChain extends Chain {
|
|
|
1464
1457
|
level: 'debug',
|
|
1465
1458
|
startFields: { target: this.describeTarget(target) },
|
|
1466
1459
|
}, async () => {
|
|
1467
|
-
|
|
1460
|
+
try {
|
|
1461
|
+
return await getHtmlFromUrl(this.logger, target.url);
|
|
1462
|
+
}
|
|
1463
|
+
catch (error) {
|
|
1464
|
+
this.logger.error({
|
|
1465
|
+
event: 'crawl.list.fetch.failed',
|
|
1466
|
+
taskId: this.taskId,
|
|
1467
|
+
data: {
|
|
1468
|
+
target: this.describeTarget(target),
|
|
1469
|
+
url: target.url,
|
|
1470
|
+
error: toErrorMessage(error),
|
|
1471
|
+
},
|
|
1472
|
+
});
|
|
1473
|
+
return '';
|
|
1474
|
+
}
|
|
1468
1475
|
});
|
|
1469
1476
|
}
|
|
1470
1477
|
async parseListPageHtml(target, listPageHtml) {
|
|
@@ -1477,10 +1484,26 @@ class CrawlingChain extends Chain {
|
|
|
1477
1484
|
},
|
|
1478
1485
|
doneFields: (items) => ({ count: items.length }),
|
|
1479
1486
|
}, async () => {
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1487
|
+
if (listPageHtml.length === 0) {
|
|
1488
|
+
return [];
|
|
1489
|
+
}
|
|
1490
|
+
try {
|
|
1491
|
+
return (await target.parseList(listPageHtml)).map((item) => ({
|
|
1492
|
+
...item,
|
|
1493
|
+
pipelineId: node_crypto.randomUUID(),
|
|
1494
|
+
}));
|
|
1495
|
+
}
|
|
1496
|
+
catch (error) {
|
|
1497
|
+
this.logger.error({
|
|
1498
|
+
event: 'crawl.list.parse.failed',
|
|
1499
|
+
taskId: this.taskId,
|
|
1500
|
+
data: {
|
|
1501
|
+
target: this.describeTarget(target),
|
|
1502
|
+
error: toErrorMessage(error),
|
|
1503
|
+
},
|
|
1504
|
+
});
|
|
1505
|
+
return [];
|
|
1506
|
+
}
|
|
1484
1507
|
});
|
|
1485
1508
|
}
|
|
1486
1509
|
async dedupeListItems(target, parsedList) {
|
|
@@ -1509,16 +1532,44 @@ class CrawlingChain extends Chain {
|
|
|
1509
1532
|
target: this.describeTarget(target),
|
|
1510
1533
|
count: list.length,
|
|
1511
1534
|
},
|
|
1512
|
-
doneFields: (
|
|
1535
|
+
doneFields: (result) => ({
|
|
1536
|
+
successCount: result.detailPagesHtmlWithPipelineId.length,
|
|
1537
|
+
failedCount: result.failedCount,
|
|
1538
|
+
}),
|
|
1513
1539
|
}, async () => {
|
|
1514
|
-
const
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1540
|
+
const settled = await Promise.allSettled(list.map((data) => getHtmlFromUrl(this.logger, data.detailUrl)));
|
|
1541
|
+
const detailPagesHtmlWithPipelineId = [];
|
|
1542
|
+
const successList = [];
|
|
1543
|
+
let failedCount = 0;
|
|
1544
|
+
settled.forEach((result, index) => {
|
|
1545
|
+
const item = list[index];
|
|
1546
|
+
if (result.status === 'fulfilled') {
|
|
1547
|
+
detailPagesHtmlWithPipelineId.push({
|
|
1548
|
+
pipelineId: item.pipelineId,
|
|
1549
|
+
html: result.value,
|
|
1550
|
+
});
|
|
1551
|
+
successList.push(item);
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
failedCount += 1;
|
|
1555
|
+
this.logger.error({
|
|
1556
|
+
event: 'crawl.detail.fetch.failed',
|
|
1557
|
+
taskId: this.taskId,
|
|
1558
|
+
data: {
|
|
1559
|
+
target: this.describeTarget(target),
|
|
1560
|
+
detailUrl: item.detailUrl,
|
|
1561
|
+
error: toErrorMessage(result.reason),
|
|
1562
|
+
},
|
|
1563
|
+
});
|
|
1564
|
+
});
|
|
1565
|
+
return {
|
|
1566
|
+
list: successList,
|
|
1567
|
+
detailPagesHtmlWithPipelineId,
|
|
1568
|
+
failedCount,
|
|
1569
|
+
};
|
|
1519
1570
|
});
|
|
1520
1571
|
}
|
|
1521
|
-
async parseDetailPagesHtml(target, detailPagesHtmlWithPipelineId) {
|
|
1572
|
+
async parseDetailPagesHtml(target, list, detailPagesHtmlWithPipelineId) {
|
|
1522
1573
|
return this.executeWithLogging({
|
|
1523
1574
|
event: 'crawl.detail.parse',
|
|
1524
1575
|
level: 'debug',
|
|
@@ -1526,13 +1577,46 @@ class CrawlingChain extends Chain {
|
|
|
1526
1577
|
target: this.describeTarget(target),
|
|
1527
1578
|
count: detailPagesHtmlWithPipelineId.length,
|
|
1528
1579
|
},
|
|
1529
|
-
doneFields: (
|
|
1580
|
+
doneFields: (result) => ({
|
|
1581
|
+
successCount: result.parsedDetails.length,
|
|
1582
|
+
failedCount: result.failedCount,
|
|
1583
|
+
}),
|
|
1530
1584
|
}, async () => {
|
|
1531
|
-
const
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1585
|
+
const listItemMap = new Map(list.map((item) => [item.pipelineId, item]));
|
|
1586
|
+
const settled = await Promise.allSettled(detailPagesHtmlWithPipelineId.map(({ html }) => target.parseDetail(html)));
|
|
1587
|
+
const parsedDetails = [];
|
|
1588
|
+
const successList = [];
|
|
1589
|
+
let failedCount = 0;
|
|
1590
|
+
settled.forEach((result, index) => {
|
|
1591
|
+
const htmlItem = detailPagesHtmlWithPipelineId[index];
|
|
1592
|
+
const listItem = listItemMap.get(htmlItem.pipelineId);
|
|
1593
|
+
if (result.status === 'fulfilled' && listItem) {
|
|
1594
|
+
parsedDetails.push({
|
|
1595
|
+
pipelineId: htmlItem.pipelineId,
|
|
1596
|
+
...result.value,
|
|
1597
|
+
});
|
|
1598
|
+
successList.push(listItem);
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
failedCount += 1;
|
|
1602
|
+
this.logger.error({
|
|
1603
|
+
event: 'crawl.detail.parse.failed',
|
|
1604
|
+
taskId: this.taskId,
|
|
1605
|
+
data: {
|
|
1606
|
+
target: this.describeTarget(target),
|
|
1607
|
+
detailUrl: listItem?.detailUrl,
|
|
1608
|
+
pipelineId: htmlItem.pipelineId,
|
|
1609
|
+
error: result.status === 'rejected'
|
|
1610
|
+
? toErrorMessage(result.reason)
|
|
1611
|
+
: 'Missing list item for parsed detail',
|
|
1612
|
+
},
|
|
1613
|
+
});
|
|
1614
|
+
});
|
|
1615
|
+
return {
|
|
1616
|
+
list: successList,
|
|
1617
|
+
parsedDetails,
|
|
1618
|
+
failedCount,
|
|
1619
|
+
};
|
|
1536
1620
|
});
|
|
1537
1621
|
}
|
|
1538
1622
|
// Although this is a synchronous method, using async wrapping to maintain consistency with the executeWithLogging interface
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/index.js
CHANGED
|
@@ -836,8 +836,14 @@ class AnalysisChain extends Chain {
|
|
|
836
836
|
}
|
|
837
837
|
}
|
|
838
838
|
|
|
839
|
+
function preprocessBoldSyntax(markdown) {
|
|
840
|
+
// Convert **text** to <strong>text</strong> before marked parsing
|
|
841
|
+
// This fixes issues where marked doesn't properly handle bold syntax with parentheses
|
|
842
|
+
return markdown.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
843
|
+
}
|
|
839
844
|
function markdownToHtml(markdown) {
|
|
840
|
-
const
|
|
845
|
+
const preprocessed = preprocessBoldSyntax(markdown);
|
|
846
|
+
const html = marked.parse(preprocessed);
|
|
841
847
|
const window = new JSDOM('').window;
|
|
842
848
|
const purify = DOMPurify(window);
|
|
843
849
|
const sanitized = purify.sanitize(html);
|
|
@@ -847,22 +853,9 @@ function markdownToHtml(markdown) {
|
|
|
847
853
|
return correctUnconvertedBoldSyntax(withDelReplaced);
|
|
848
854
|
}
|
|
849
855
|
function addTargetBlankToAnchors(htmlString) {
|
|
850
|
-
//
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
const regex = /<a(\s+[^>]*?)?(?<!target="[^"]*")>/gi;
|
|
854
|
-
// Use regex to find '<a>' tags and add 'target="_blank"'
|
|
855
|
-
return htmlString.replace(regex, (_match, attributes) => {
|
|
856
|
-
// Handle undefined attributes as empty string
|
|
857
|
-
const currentAttributes = attributes || '';
|
|
858
|
-
// Double check if target attribute exists (safety check for regex limitations)
|
|
859
|
-
if (currentAttributes.includes('target=')) {
|
|
860
|
-
return `<a${currentAttributes}>`; // If target attribute exists, return without modification
|
|
861
|
-
}
|
|
862
|
-
else {
|
|
863
|
-
// Add target="_blank" attribute
|
|
864
|
-
return `<a${currentAttributes} target="_blank">`;
|
|
865
|
-
}
|
|
856
|
+
// DOMPurify removes target attributes, so we can safely add target="_blank" to all anchors
|
|
857
|
+
return htmlString.replace(/<a\s+([^>]*)>/gi, (_match, attributes) => {
|
|
858
|
+
return `<a ${attributes} target="_blank">`;
|
|
866
859
|
});
|
|
867
860
|
}
|
|
868
861
|
function replaceDelTagsWithTilde(htmlString) {
|
|
@@ -875,19 +868,20 @@ function correctUnconvertedBoldSyntax(htmlString) {
|
|
|
875
868
|
return htmlString.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>');
|
|
876
869
|
}
|
|
877
870
|
function correctMalformedUrls(htmlString) {
|
|
878
|
-
// Pattern matches anchors with `)` followed by URL-encoded characters
|
|
871
|
+
// Pattern matches anchors with `)` followed by optional closing markup (</b> or **) and URL-encoded characters
|
|
879
872
|
// Capture groups:
|
|
880
873
|
// 1: attributes before href
|
|
881
874
|
// 2: URL base (before `)`)
|
|
882
|
-
// 3:
|
|
883
|
-
// 4:
|
|
884
|
-
// 5:
|
|
885
|
-
// 6:
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
//
|
|
890
|
-
|
|
875
|
+
// 3: closing markup (</b> or ** or empty)
|
|
876
|
+
// 4: URL-encoded part (starts with %)
|
|
877
|
+
// 5: attributes after href
|
|
878
|
+
// 6: link text base (before `)`)
|
|
879
|
+
// 7: text after `)` in link text (may include **)
|
|
880
|
+
const regex = /<a\s+([^>]*?)href="([^"]*?)\)((?:<\/b>|\*\*)?)(%[0-9A-Fa-f]{2}[^"]*?)"([^>]*?)>([^<]*?)\)((?:\*\*)?[^<]*?)<\/a>/g;
|
|
881
|
+
return htmlString.replace(regex, (_match, beforeHref, urlBase, closingMarkup, _encodedPart, afterHref, textBase, textAfterClosingParen) => {
|
|
882
|
+
// Remove leading ** from textAfterClosingParen since it's already captured as closingMarkup (</b>)
|
|
883
|
+
const cleanedText = textAfterClosingParen.replace(/^\*\*/, '');
|
|
884
|
+
return `<a ${beforeHref}href="${urlBase}"${afterHref}>${textBase}</a>${closingMarkup})${cleanedText}`;
|
|
891
885
|
});
|
|
892
886
|
}
|
|
893
887
|
|
|
@@ -1395,6 +1389,7 @@ async function getHtmlFromUrl(logger, url, referer = 'https://www.google.com/')
|
|
|
1395
1389
|
throw lastError;
|
|
1396
1390
|
}
|
|
1397
1391
|
|
|
1392
|
+
const toErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
1398
1393
|
class CrawlingChain extends Chain {
|
|
1399
1394
|
constructor(config) {
|
|
1400
1395
|
const provider = config.provider;
|
|
@@ -1424,17 +1419,15 @@ class CrawlingChain extends Chain {
|
|
|
1424
1419
|
target: ({ target }) => target,
|
|
1425
1420
|
})
|
|
1426
1421
|
.pipe({
|
|
1427
|
-
|
|
1422
|
+
detailFetchResult: ({ target, list }) => this.fetchDetailPagesHtml(target, list),
|
|
1428
1423
|
target: ({ target }) => target,
|
|
1429
|
-
list: ({ list }) => list,
|
|
1430
1424
|
})
|
|
1431
1425
|
.pipe({
|
|
1432
|
-
|
|
1426
|
+
detailParseResult: ({ target, detailFetchResult }) => this.parseDetailPagesHtml(target, detailFetchResult.list, detailFetchResult.detailPagesHtmlWithPipelineId),
|
|
1433
1427
|
target: ({ target }) => target,
|
|
1434
|
-
list: ({ list }) => list,
|
|
1435
1428
|
})
|
|
1436
1429
|
.pipe({
|
|
1437
|
-
processedArticles: ({ target,
|
|
1430
|
+
processedArticles: ({ target, detailParseResult }) => this.mergeParsedArticles(target, detailParseResult.list, detailParseResult.parsedDetails),
|
|
1438
1431
|
target: ({ target }) => target,
|
|
1439
1432
|
})
|
|
1440
1433
|
.pipe({
|
|
@@ -1462,7 +1455,21 @@ class CrawlingChain extends Chain {
|
|
|
1462
1455
|
level: 'debug',
|
|
1463
1456
|
startFields: { target: this.describeTarget(target) },
|
|
1464
1457
|
}, async () => {
|
|
1465
|
-
|
|
1458
|
+
try {
|
|
1459
|
+
return await getHtmlFromUrl(this.logger, target.url);
|
|
1460
|
+
}
|
|
1461
|
+
catch (error) {
|
|
1462
|
+
this.logger.error({
|
|
1463
|
+
event: 'crawl.list.fetch.failed',
|
|
1464
|
+
taskId: this.taskId,
|
|
1465
|
+
data: {
|
|
1466
|
+
target: this.describeTarget(target),
|
|
1467
|
+
url: target.url,
|
|
1468
|
+
error: toErrorMessage(error),
|
|
1469
|
+
},
|
|
1470
|
+
});
|
|
1471
|
+
return '';
|
|
1472
|
+
}
|
|
1466
1473
|
});
|
|
1467
1474
|
}
|
|
1468
1475
|
async parseListPageHtml(target, listPageHtml) {
|
|
@@ -1475,10 +1482,26 @@ class CrawlingChain extends Chain {
|
|
|
1475
1482
|
},
|
|
1476
1483
|
doneFields: (items) => ({ count: items.length }),
|
|
1477
1484
|
}, async () => {
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1485
|
+
if (listPageHtml.length === 0) {
|
|
1486
|
+
return [];
|
|
1487
|
+
}
|
|
1488
|
+
try {
|
|
1489
|
+
return (await target.parseList(listPageHtml)).map((item) => ({
|
|
1490
|
+
...item,
|
|
1491
|
+
pipelineId: randomUUID(),
|
|
1492
|
+
}));
|
|
1493
|
+
}
|
|
1494
|
+
catch (error) {
|
|
1495
|
+
this.logger.error({
|
|
1496
|
+
event: 'crawl.list.parse.failed',
|
|
1497
|
+
taskId: this.taskId,
|
|
1498
|
+
data: {
|
|
1499
|
+
target: this.describeTarget(target),
|
|
1500
|
+
error: toErrorMessage(error),
|
|
1501
|
+
},
|
|
1502
|
+
});
|
|
1503
|
+
return [];
|
|
1504
|
+
}
|
|
1482
1505
|
});
|
|
1483
1506
|
}
|
|
1484
1507
|
async dedupeListItems(target, parsedList) {
|
|
@@ -1507,16 +1530,44 @@ class CrawlingChain extends Chain {
|
|
|
1507
1530
|
target: this.describeTarget(target),
|
|
1508
1531
|
count: list.length,
|
|
1509
1532
|
},
|
|
1510
|
-
doneFields: (
|
|
1533
|
+
doneFields: (result) => ({
|
|
1534
|
+
successCount: result.detailPagesHtmlWithPipelineId.length,
|
|
1535
|
+
failedCount: result.failedCount,
|
|
1536
|
+
}),
|
|
1511
1537
|
}, async () => {
|
|
1512
|
-
const
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1538
|
+
const settled = await Promise.allSettled(list.map((data) => getHtmlFromUrl(this.logger, data.detailUrl)));
|
|
1539
|
+
const detailPagesHtmlWithPipelineId = [];
|
|
1540
|
+
const successList = [];
|
|
1541
|
+
let failedCount = 0;
|
|
1542
|
+
settled.forEach((result, index) => {
|
|
1543
|
+
const item = list[index];
|
|
1544
|
+
if (result.status === 'fulfilled') {
|
|
1545
|
+
detailPagesHtmlWithPipelineId.push({
|
|
1546
|
+
pipelineId: item.pipelineId,
|
|
1547
|
+
html: result.value,
|
|
1548
|
+
});
|
|
1549
|
+
successList.push(item);
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
failedCount += 1;
|
|
1553
|
+
this.logger.error({
|
|
1554
|
+
event: 'crawl.detail.fetch.failed',
|
|
1555
|
+
taskId: this.taskId,
|
|
1556
|
+
data: {
|
|
1557
|
+
target: this.describeTarget(target),
|
|
1558
|
+
detailUrl: item.detailUrl,
|
|
1559
|
+
error: toErrorMessage(result.reason),
|
|
1560
|
+
},
|
|
1561
|
+
});
|
|
1562
|
+
});
|
|
1563
|
+
return {
|
|
1564
|
+
list: successList,
|
|
1565
|
+
detailPagesHtmlWithPipelineId,
|
|
1566
|
+
failedCount,
|
|
1567
|
+
};
|
|
1517
1568
|
});
|
|
1518
1569
|
}
|
|
1519
|
-
async parseDetailPagesHtml(target, detailPagesHtmlWithPipelineId) {
|
|
1570
|
+
async parseDetailPagesHtml(target, list, detailPagesHtmlWithPipelineId) {
|
|
1520
1571
|
return this.executeWithLogging({
|
|
1521
1572
|
event: 'crawl.detail.parse',
|
|
1522
1573
|
level: 'debug',
|
|
@@ -1524,13 +1575,46 @@ class CrawlingChain extends Chain {
|
|
|
1524
1575
|
target: this.describeTarget(target),
|
|
1525
1576
|
count: detailPagesHtmlWithPipelineId.length,
|
|
1526
1577
|
},
|
|
1527
|
-
doneFields: (
|
|
1578
|
+
doneFields: (result) => ({
|
|
1579
|
+
successCount: result.parsedDetails.length,
|
|
1580
|
+
failedCount: result.failedCount,
|
|
1581
|
+
}),
|
|
1528
1582
|
}, async () => {
|
|
1529
|
-
const
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1583
|
+
const listItemMap = new Map(list.map((item) => [item.pipelineId, item]));
|
|
1584
|
+
const settled = await Promise.allSettled(detailPagesHtmlWithPipelineId.map(({ html }) => target.parseDetail(html)));
|
|
1585
|
+
const parsedDetails = [];
|
|
1586
|
+
const successList = [];
|
|
1587
|
+
let failedCount = 0;
|
|
1588
|
+
settled.forEach((result, index) => {
|
|
1589
|
+
const htmlItem = detailPagesHtmlWithPipelineId[index];
|
|
1590
|
+
const listItem = listItemMap.get(htmlItem.pipelineId);
|
|
1591
|
+
if (result.status === 'fulfilled' && listItem) {
|
|
1592
|
+
parsedDetails.push({
|
|
1593
|
+
pipelineId: htmlItem.pipelineId,
|
|
1594
|
+
...result.value,
|
|
1595
|
+
});
|
|
1596
|
+
successList.push(listItem);
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
failedCount += 1;
|
|
1600
|
+
this.logger.error({
|
|
1601
|
+
event: 'crawl.detail.parse.failed',
|
|
1602
|
+
taskId: this.taskId,
|
|
1603
|
+
data: {
|
|
1604
|
+
target: this.describeTarget(target),
|
|
1605
|
+
detailUrl: listItem?.detailUrl,
|
|
1606
|
+
pipelineId: htmlItem.pipelineId,
|
|
1607
|
+
error: result.status === 'rejected'
|
|
1608
|
+
? toErrorMessage(result.reason)
|
|
1609
|
+
: 'Missing list item for parsed detail',
|
|
1610
|
+
},
|
|
1611
|
+
});
|
|
1612
|
+
});
|
|
1613
|
+
return {
|
|
1614
|
+
list: successList,
|
|
1615
|
+
parsedDetails,
|
|
1616
|
+
failedCount,
|
|
1617
|
+
};
|
|
1534
1618
|
});
|
|
1535
1619
|
}
|
|
1536
1620
|
// Although this is a synchronous method, using async wrapping to maintain consistency with the executeWithLogging interface
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@llm-newsletter-kit/core",
|
|
3
3
|
"private": false,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "1.1.
|
|
5
|
+
"version": "1.1.6",
|
|
6
6
|
"description": "An extensible framework to automate your entire newsletter workflow. Handles data collection, LLM-based content analysis, and email generation, letting you focus on your unique domain logic.",
|
|
7
7
|
"main": "dist/index.cjs",
|
|
8
8
|
"module": "dist/index.js",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"author": "kimhongyeon",
|
|
47
47
|
"license": "Apache-2.0",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@langchain/core": "^1.1.
|
|
50
|
-
"ai": "^6.0.
|
|
49
|
+
"@langchain/core": "^1.1.19",
|
|
50
|
+
"ai": "^6.0.69",
|
|
51
51
|
"dompurify": "^3.3.1",
|
|
52
52
|
"es-toolkit": "^1.44.0",
|
|
53
53
|
"jsdom": "^27.4.0",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"@eslint/js": "^9.39.2",
|
|
60
60
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
61
61
|
"@types/jsdom": "^27.0.0",
|
|
62
|
-
"@types/node": "^25.
|
|
62
|
+
"@types/node": "^25.2.0",
|
|
63
63
|
"@vitest/coverage-v8": "^3.2.4",
|
|
64
64
|
"@vitest/expect": "^3.2.4",
|
|
65
65
|
"eslint": "^9.39.2",
|