From 0f184a37d49cbf1ac0d759aa41fe3b3c1ef25f10 Mon Sep 17 00:00:00 2001 From: pseudoyu Date: Fri, 16 Oct 2026 20:05:22 +0800 Subject: [PATCH] feat(route/gfqh): add research reports Reports from the GF Futures research site, filtered by the site's own variety and report type codes. The reports are mostly PDF attachments, so each item lists the attachment links after the article body. Closes #23604 --- lib/routes/gfqh/namespace.ts | 8 +++ lib/routes/gfqh/report.tsx | 100 +++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 lib/routes/gfqh/namespace.ts create mode 100644 lib/routes/gfqh/report.tsx diff --git a/lib/routes/gfqh/namespace.ts b/lib/routes/gfqh/namespace.ts new file mode 100644 index 0000000000..c43e761d94 --- /dev/null +++ b/lib/routes/gfqh/namespace.ts @@ -0,0 +1,8 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: '广发期货', + url: 'rd.gfqh.cn', + categories: ['finance'], + lang: 'zh-CN', +}; diff --git a/lib/routes/gfqh/report.tsx b/lib/routes/gfqh/report.tsx new file mode 100644 index 0000000000..ae3218d943 --- /dev/null +++ b/lib/routes/gfqh/report.tsx @@ -0,0 +1,100 @@ +import { load } from 'cheerio'; +import { raw } from 'hono/html'; +import { renderToString } from 'hono/jsx/dom/server'; +import pMap from 'p-map'; + +import InvalidParameterError from '@/errors/types/invalid-parameter'; +import type { DataItem, Route } from '@/types'; +import cache from '@/utils/cache'; +import ofetch from '@/utils/ofetch'; +import { parseDate } from '@/utils/parse-date'; +import timezone from '@/utils/timezone'; + +const baseUrl = 'https://rd.gfqh.cn/RDInformation'; + +const varieties = { '0': '全部', '100009': '综合', '100010': '黑色金属', '100011': '有色金属', '100002': '能源化工', '100004': '金融', '100005': '农产品' }; +const categories = { '0': '全部', '100005': '调研报告', '100006': '热点点评', '100001': '常规报告', '100003': '专题报告', '100004': '其他报告' }; + +export const route: Route = { + path: '/report/:variety?/:category?', + categories: ['finance'], + example: '/gfqh/report/100005', + parameters: { + variety: { description: '交易品种', default: '0', options: Object.entries(varieties).map(([value, label]) => ({ value, label })) }, + category: { description: '报告类型', default: '0', options: Object.entries(categories).map(([value, label]) => ({ value, label })) }, + }, + radar: [ + { + source: ['rd.gfqh.cn/RDInformation/menu/235'], + target: '/report', + }, + ], + name: '研究报告', + maintainers: ['pseudoyu'], + handler, + url: 'rd.gfqh.cn/RDInformation/menu/235', + description: '例如农产品常规报告使用 `/gfqh/report/100005/100001`。报告正文多为 PDF 附件,附件链接列在条目内容末尾。只读取第一页。', +}; + +async function handler(ctx) { + const { variety = '0', category = '0' } = ctx.req.param(); + if (!Object.hasOwn(varieties, variety) || !Object.hasOwn(categories, category)) { + throw new InvalidParameterError('Use a variety and a report type code listed in the route parameters.'); + } + + const response = await ofetch(`${baseUrl}/article/report/list`, { query: { v: variety, c: category, p: 1 } }); + const $ = load(response); + + const list = $('.article-item') + .toArray() + .map((element) => { + const item = $(element); + return { + title: item.find('.article-title').text().trim(), + link: `${baseUrl}/article/${item.attr('onclick')?.match(/article\/(\d+)/)?.[1]}`, + pubDate: timezone(parseDate(item.find('.article-publish-date').text().trim()), 8), + }; + }); + + const items = await pMap( + list, + (item) => + cache.tryGet(item.link, async (): Promise => { + const detailResponse = await ofetch(item.link); + const detail = load(detailResponse); + const attachments = detail('.attachment-item') + .toArray() + .map((element) => ({ title: detail(element).find('.attachment-title').text().trim(), url: detail(element).attr('data-url') })); + + return { + ...item, + category: detail('.future-daily-report span:not(:has(span))') + .toArray() + .map((element) => detail(element).text().trim()), + description: renderToString( + <> + {raw(detail('.article-content').html() ?? '')} + {attachments.length ? ( + + ) : null} + + ), + }; + }), + { concurrency: 3 } + ); + + const names = [varieties[variety], categories[category]].filter((name) => name !== '全部'); + return { + title: ['广发期货', '研究报告', ...names].join(' - '), + link: `${baseUrl}/menu/235`, + language: 'zh-CN' as const, + item: items, + }; +}