Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 123 additions & 60 deletions lib/routes/openai/cookbook.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,141 @@
import type { Context } from 'hono';
import { load } from 'cheerio';

import { config } from '@/config';
import type { DataItem, Route } from '@/types';
import cache from '@/utils/cache';
import logger from '@/utils/logger';
import { parseDate } from '@/utils/parse-date';
import ofetch from '@/utils/ofetch';

export const route: Route = {
path: '/cookbook',
categories: ['programming'],
description:
'OpenAI Cookbook 提供了大量使用 OpenAI API 的实用指南和示例代码,涵盖了从基础到高级的各种主题,包括 GPT 模型、嵌入、函数调用、微调等。这里汇集了最新的 API 功能介绍和流行的应用案例,是开发者学习和应用 OpenAI 技术的宝贵资源。',
maintainers: ['liyaozhong'],
example: '/openai/cookbook',
parameters: {
limit: '返回的文章数量,默认 30',
},
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: true,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['cookbook.openai.com/'],
source: ['developers.openai.com/cookbook', 'cookbook.openai.com/'],
},
],
url: 'cookbook.openai.com/',
handler,
example: '/openai/cookbook',
url: 'developers.openai.com/cookbook',
name: 'Cookbook',
description: 'OpenAI Cookbook 提供了大量使用 OpenAI API 的实用指南和示例代码,涵盖了从基础到高级的各种主题,包括 GPT 模型、嵌入、函数调用、微调等。这里汇集了最新的 API 功能介绍和流行的应用案例,是开发者学习和应用 OpenAI 技术的宝贵资源。',
maintainers: ['liyaozhong'],
handler,
};

async function handler() {
const rootUrl = 'https://cookbook.openai.com';
const currentUrl = `${rootUrl}/`;

try {
const response = await ofetch(currentUrl);
const $ = load(response);

let items = $('[class="min-h-[90vh] mt-4"] .grid a')
.toArray()
.map((element): DataItem => {
const $element = $(element);
const $title = $element.find('div.font-semibold.text-sm.text-primary.line-clamp-1.overflow-ellipsis');
const $date = $element.find(String.raw`span.text-xs.text-muted-foreground.md\:w-24.text-end`);
const $author = $element.find('p:contains("OpenAI")');
const $tags = $element.find('span[style^="color:"]');

return {
title: $title.text().trim(),
link: `${rootUrl}/${$element.attr('href')}`,
pubDate: $date.text().trim(),
author: $author.text().replace('OpenAI', '').trim(),
category: $tags.toArray().map((tag) => $(tag).text().trim()),
};
});

items = (
await Promise.all(
items.map((item) =>
cache.tryGet(item.link!, async () => {
try {
const detailResponse = await ofetch(item.link!);
const $ = load(detailResponse);

item.description = $(String.raw`article.prose.prose-sm.sm\:prose-base.max-w-none.dark\:prose-invert`).html();
return item;
} catch {
return { ...item, description: '' };
}
})
)
)
).filter((item) => item?.description);

return {
title: 'OpenAI Cookbook',
link: currentUrl,
item: items,
};
} catch (error) {
logger.error(`处理 OpenAI Cookbook 请求时发生错误: ${error}`);
throw error;
// 递归解码 Astro 组件 props 的序列化格式
// [0, value] → 标量;[1, [...]] → 数组;普通对象递归解码每个属性
function decodeAstroValue(val: unknown): unknown {
if (Array.isArray(val)) {
if (val.length === 2 && (val[0] === 0 || val[0] === 1)) {
if (val[0] === 0) {
return decodeAstroValue(val[1]);
}
if (Array.isArray(val[1])) {
return val[1].map(decodeAstroValue);
}
}
return val;
}
if (val !== null && typeof val === 'object') {
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(val)) {
result[key] = decodeAstroValue(value);
}
return result;
}
return val;
}

interface CookbookEntry {
title: string;
date: string;
slug: string;
internal_path: string;
description: string;
tags: string[];
authors?: { name: string }[];
archived: boolean;
}

async function handler(ctx: Context) {
const limit = Number.parseInt(ctx.req.query('limit') || '30');
const rootUrl = 'https://developers.openai.com';
const currentUrl = `${rootUrl}/cookbook/`;

const response = await ofetch(currentUrl, {
headers: { 'User-Agent': config.ua },
});
const $ = load(response);

// 从 astro-island 的 props 属性中提取文章数据
const propsRaw = $('astro-island[component-url*="CookbookSearchPage"]').attr('props');

if (!propsRaw) {
throw new Error('无法从页面中提取 CookbookSearchPage 组件数据');
}

// 解析 astro-island props JSON
const props = JSON.parse(propsRaw) as { entries: [number, unknown[]] };
const entriesArray = props.entries[1];

if (!Array.isArray(entriesArray)) {
throw new Error('CookbookSearchPage entries 格式异常');
}

const articles = entriesArray
.map((item) => decodeAstroValue(item) as CookbookEntry | null)
.filter((item): item is CookbookEntry => item !== null && typeof item === 'object' && 'title' in item)
.filter((item) => !item.archived)
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());

const items = await Promise.all(
articles.slice(0, limit).map((item) =>
cache.tryGet(`${rootUrl}/cookbook/${item.internal_path}`, async () => {
const dataItem: DataItem = {
title: item.title,
link: `${rootUrl}/cookbook/${item.internal_path}`,
pubDate: parseDate(item.date),
category: item.tags,
description: item.description,
};

if (item.authors?.length > 0) {

Check failure on line 114 in lib/routes/openai/cookbook.ts

View workflow job for this annotation

GitHub Actions / Lint

'item.authors.length' is possibly 'undefined'.
dataItem.author = item.authors.map((a) => a.name).filter(Boolean).join(', ');

Check failure on line 115 in lib/routes/openai/cookbook.ts

View workflow job for this annotation

GitHub Actions / Lint

'item.authors' is possibly 'undefined'.
}

try {
const detailResponse = await ofetch(dataItem.link!, {
headers: { 'User-Agent': config.ua },
});
const $detail = load(detailResponse);
const content = $detail('article').first().html();
if (content) {
dataItem.description = content;
}
} catch {
// 保留 props 中的 description 作为后备
}

return dataItem;
})
)
);

return {
title: 'OpenAI Cookbook',
link: currentUrl,
item: items,
};
}
Loading