Skip to content

Commit 778cbb5

Browse files
committed
Add ChatGPT Atlas release notes route
- Add route: /openai/chatgpt-atlas/release-notes - Parse release notes from OpenAI help center - Use Puppeteer to bypass Cloudflare protection - Extract build version, date, and content sections - Cache results for 24 hours - Proper resource cleanup (browser.close())
1 parent 4291a29 commit 778cbb5

File tree

1 file changed

+35
-99
lines changed

1 file changed

+35
-99
lines changed

lib/routes/openai/chatgpt-atlas.ts

Lines changed: 35 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import { load } from 'cheerio';
22
import dayjs from 'dayjs';
33

4+
import { config } from '@/config';
45
import type { DataItem, Route } from '@/types';
56
import cache from '@/utils/cache';
6-
import puppeteer from '@/utils/puppeteer';
7+
import ofetch from '@/utils/ofetch';
78

89
export const route: Route = {
910
path: '/chatgpt-atlas/release-notes',
1011
categories: ['program-update'],
1112
example: '/openai/chatgpt-atlas/release-notes',
1213
features: {
1314
requireConfig: false,
14-
requirePuppeteer: true,
15+
requirePuppeteer: false,
1516
antiCrawler: false,
1617
supportBT: false,
1718
supportPodcast: false,
@@ -28,117 +29,52 @@ async function handler() {
2829
const cacheIn = await cache.tryGet(
2930
articleUrl,
3031
async () => {
31-
const browser = await puppeteer();
32-
const page = await browser.newPage();
33-
await page.setRequestInterception(true);
34-
page.on('request', (request) => {
35-
request.resourceType() === 'document' || request.resourceType() === 'script' ? request.continue() : request.abort();
36-
});
37-
await page.goto(articleUrl, {
38-
waitUntil: 'domcontentloaded',
39-
});
40-
const html = await page.evaluate(() => document.documentElement.innerHTML);
41-
await page.close();
42-
43-
const $ = load(html);
32+
const response = await ofetch(articleUrl);
33+
const $ = load(response);
4434
const articleContent = $('.article-content');
4535

4636
if (articleContent.length === 0) {
47-
throw new Error('Failed to find article content. Possible Cloudflare protection.');
37+
throw new Error('Failed to find article content.');
4838
}
4939

5040
const feedTitle = $('h1').first().text();
5141
const feedDesc = 'ChatGPT Atlas Release Notes';
5242

53-
const items: DataItem[] = [];
54-
55-
articleContent.children('h1').each((_, element) => {
56-
const $h1 = $(element);
57-
const text = $h1.text().trim();
58-
59-
const dateMatch = text.match(/(\w+\s+\d+[stndrh]*,\s+\d{4})/i);
60-
let pubDate: Date | undefined;
61-
if (dateMatch) {
62-
const dateStr = dateMatch[1];
63-
const parsedDate = dayjs(dateStr, ['MMMM Do, YYYY', 'MMMM D, YYYY'], 'en');
64-
if (parsedDate.isValid()) {
65-
pubDate = parsedDate.toDate();
66-
}
67-
}
68-
69-
const buildMatch = text.match(/(?:Build\s*Number\s*:|Build\s*:)\s*(\d+\.\d+\.\d+\.\d+)/i);
70-
let buildInfo = '';
71-
let titleText = text;
72-
73-
if (buildMatch) {
74-
buildInfo = `<p>Build: ${buildMatch[1]}</p>`;
75-
titleText = text.replace(buildMatch[0], '').trim();
76-
}
77-
78-
let description = buildInfo;
79-
80-
$h1.nextUntil('h1').each((_, sib) => {
81-
const $sib = $(sib);
82-
const tagName = sib.tagName.toLowerCase();
83-
84-
switch (tagName) {
85-
case 'h2':
86-
case 'h3':
87-
case 'h4':
88-
case 'h5':
89-
case 'h6':
90-
description += `<${tagName}>${$sib.html()?.trim() || ''}</${tagName}>`;
91-
break;
92-
93-
case 'ul':
94-
case 'ol': {
95-
const listItems = $sib
96-
.find('li')
97-
.toArray()
98-
.map((li) => {
99-
const $li = $(li);
100-
const liHtml = $li.html()?.trim();
101-
return liHtml ? `<li>${liHtml}</li>` : null;
102-
})
103-
.filter(Boolean);
104-
if (listItems.length > 0) {
105-
description += `<${tagName}>${listItems.join('')}</${tagName}>`;
106-
}
107-
break;
108-
}
109-
case 'p': {
110-
const pHtml = $sib.html()?.trim();
111-
if (pHtml && !pHtml.toLowerCase().includes('public link to lgpl bundle')) {
112-
description += `<p>${pHtml}</p>`;
113-
}
114-
break;
115-
}
116-
case 'br':
117-
description += '<br>';
118-
break;
119-
default: {
120-
const html = $sib.html()?.trim();
121-
if (html) {
122-
description += html;
123-
}
43+
const items = $('h1', articleContent)
44+
.toArray()
45+
.map((element) => {
46+
const $h1 = $(element);
47+
const text = $h1.text().trim();
48+
49+
const dateMatch = text.match(/(\w+\s+\d+[stndrh]*,\s+\d{4})/i);
50+
let pubDate: Date | undefined;
51+
if (dateMatch) {
52+
const dateStr = dateMatch[1];
53+
const parsedDate = dayjs(dateStr, ['MMMM Do, YYYY', 'MMMM D, YYYY'], 'en');
54+
if (parsedDate.isValid()) {
55+
pubDate = parsedDate.toDate();
12456
}
12557
}
126-
});
127-
128-
items.push({
129-
guid: `${articleUrl}#${pubDate ? pubDate.getTime() : titleText}`,
130-
title: titleText,
131-
link: articleUrl,
132-
pubDate,
133-
description,
134-
});
135-
});
13658

137-
await browser.close();
59+
const content = $h1
60+
.nextUntil('h1')
61+
.toArray()
62+
.map((el) => $(el).prop('outerHTML'))
63+
.join('');
64+
const description = content;
65+
66+
return {
67+
guid: `${articleUrl}#${pubDate ? pubDate.getTime() : text}`,
68+
title: text,
69+
link: articleUrl,
70+
pubDate,
71+
description,
72+
};
73+
}) as DataItem[];
13874

13975
return { feedTitle, feedDesc, items };
14076
},
141-
86400,
77+
config.cache.routeExpire,
14278
false
14379
);
14480

0 commit comments

Comments
 (0)