From 0191121c66a4b71aed320cd9a7a11919a5ae3c3b Mon Sep 17 00:00:00 2001 From: fishwww-ww <2470335462@qq.com> Date: Wed, 15 Oct 2025 09:22:13 +0800 Subject: [PATCH 1/2] feat: daily hot news plugin --- modules/tool/packages/dailyHot/config.ts | 50 ++++ modules/tool/packages/dailyHot/index.ts | 10 + modules/tool/packages/dailyHot/logo.svg | 20 ++ modules/tool/packages/dailyHot/package.json | 17 ++ modules/tool/packages/dailyHot/src/index.ts | 243 ++++++++++++++++++ .../tool/packages/dailyHot/test/index.test.ts | 8 + 6 files changed, 348 insertions(+) create mode 100644 modules/tool/packages/dailyHot/config.ts create mode 100644 modules/tool/packages/dailyHot/index.ts create mode 100644 modules/tool/packages/dailyHot/logo.svg create mode 100644 modules/tool/packages/dailyHot/package.json create mode 100644 modules/tool/packages/dailyHot/src/index.ts create mode 100644 modules/tool/packages/dailyHot/test/index.test.ts diff --git a/modules/tool/packages/dailyHot/config.ts b/modules/tool/packages/dailyHot/config.ts new file mode 100644 index 00000000..18489b25 --- /dev/null +++ b/modules/tool/packages/dailyHot/config.ts @@ -0,0 +1,50 @@ +import { defineTool } from '@tool/type'; +import { FlowNodeInputTypeEnum, WorkflowIOValueTypeEnum } from '@tool/type/fastgpt'; +import { ToolTypeEnum } from '@tool/type/tool'; + +export default defineTool({ + name: { + 'zh-CN': '热榜工具', + en: 'Hot List Tool' + }, + type: ToolTypeEnum.tools, + description: { + 'zh-CN': '获取热榜信息,支持36氪、知乎、微博、掘金、头条等多个平台', + en: 'Get hot list information from multiple platforms including 36kr, Zhihu, Weibo, Juejin, and Toutiao' + }, + toolDescription: + 'Get hot trending content from multiple platforms including 36kr, Zhihu, Weibo, Juejin, and Toutiao with accurate publish times', + versionList: [ + { + value: '0.1.0', + description: 'Default version', + inputs: [ + { + key: 'sources', + label: '热榜来源', + renderTypeList: [FlowNodeInputTypeEnum.multipleSelect], + valueType: WorkflowIOValueTypeEnum.arrayString, + required: true, + description: '选择热榜来源网站(可多选)', + toolDescription: '选择热榜来源网站', + defaultValue: ['36kr'], + list: [ + { label: '36氪', value: '36kr' }, + { label: '知乎', value: 'zhihu' }, + { label: '微博', value: 'weibo' }, + { label: '掘金', value: 'juejin' }, + { label: '头条', value: 'toutiao' } + ] + } + ], + outputs: [ + { + valueType: WorkflowIOValueTypeEnum.arrayObject, + key: 'hotNewsList', + label: '新闻热榜列表', + description: '新闻热榜数据列表' + } + ] + } + ] +}); diff --git a/modules/tool/packages/dailyHot/index.ts b/modules/tool/packages/dailyHot/index.ts new file mode 100644 index 00000000..d698ed48 --- /dev/null +++ b/modules/tool/packages/dailyHot/index.ts @@ -0,0 +1,10 @@ +import config from './config'; +import { InputType, OutputType, tool as toolCb } from './src'; +import { exportTool } from '@tool/utils/tool'; + +export default exportTool({ + toolCb, + InputType, + OutputType, + config +}); diff --git a/modules/tool/packages/dailyHot/logo.svg b/modules/tool/packages/dailyHot/logo.svg new file mode 100644 index 00000000..1e4b9a3d --- /dev/null +++ b/modules/tool/packages/dailyHot/logo.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/modules/tool/packages/dailyHot/package.json b/modules/tool/packages/dailyHot/package.json new file mode 100644 index 00000000..f98f7091 --- /dev/null +++ b/modules/tool/packages/dailyHot/package.json @@ -0,0 +1,17 @@ +{ + "name": "@fastgpt-plugins/tool-daily-hot", + "module": "index.ts", + "type": "module", + "scripts": { + "build": "bun ../../../../scripts/build.ts" + }, + "devDependencies": { + "@types/bun": "^1.2.2" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "zod": "^3.24.3" + } +} diff --git a/modules/tool/packages/dailyHot/src/index.ts b/modules/tool/packages/dailyHot/src/index.ts new file mode 100644 index 00000000..7c16328b --- /dev/null +++ b/modules/tool/packages/dailyHot/src/index.ts @@ -0,0 +1,243 @@ +import { z } from 'zod'; +import { POST, GET } from '@tool/utils/request'; + +export const InputType = z.object({ + sources: z.array(z.enum(['36kr', 'zhihu', 'weibo', 'juejin', 'toutiao'])).default(['36kr']) +}); + +export const OutputType = z.object({ + hotNewsList: z.array( + z.object({ + title: z.string().describe('hot news title').optional(), + description: z.string().describe('hot news description').optional(), + source: z.string().describe('hot news source website'), + time: z.string().describe('hot news publish time').optional(), + error: z.string().describe('failed to get hot news, error message').optional() + }) + ) +}); + +type Response = { + title?: string; + description?: string; + source: string; + time?: string; + error?: string; +}; + +type Kr36Response = { + data: { + hotRankList: { + templateMaterial: { + widgetTitle: string; + }; + publishTime: string; + }[]; + }; +}; + +type ZhihuResponse = { + data: { + target: { + title: string; + created: number; + excerpt: string; + }; + }[]; +}; + +type WeiboResponse = { + data: { + cards: { + card_group?: { + desc: string; + }[]; + }[]; + }; +}; + +type JuejinResponse = { + data: { + content: { + content_id: string; + title: string; + created_at: number; + ctime: number; + }; + author: { + name: string; + }; + content_counter: { + hot_rank: number; + }; + }[]; +}; + +type ToutiaoResponse = { + data: { + Title: string; + }[]; +}; + +export async function tool({ + sources +}: z.infer): Promise> { + const promises = sources.map(async (source) => { + switch (source) { + case '36kr': + return await get36krList(); + case 'zhihu': + return await getZhihuList(); + case 'weibo': + return await getWeiboList(); + case 'juejin': + return await getJuejinList(); + case 'toutiao': + return await getToutiaoList(); + default: + return []; + } + }); + const results = await Promise.allSettled(promises); + const allHotNewsList: Response[] = []; + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + allHotNewsList.push(...result.value); + } else { + allHotNewsList.push({ + error: `Failed to get hot news from ${sources[index]}`, + source: sources[index] + }); + } + }); + + if (!allHotNewsList.length) { + return Promise.reject({ + error: 'Failed to get hot news list' + }); + } + + return { + hotNewsList: allHotNewsList + }; +} + +async function get36krList(): Promise { + const url = `https://gateway.36kr.com/api/mis/nav/home/nav/rank/hot`; + + const { data: Kr36Response } = await POST(url, { + partner_id: 'wap', + param: { + siteId: 1, + platformId: 2 + }, + timestamp: new Date().getTime() + }); + const kr36List = Kr36Response?.data?.hotRankList; + if (!kr36List) { + return Promise.reject({ + error: 'Failed to get kr36 list' + }); + } + + const hotNewsList: Response[] = kr36List.map((item) => { + return { + title: item.templateMaterial.widgetTitle, + source: '36氪', + time: new Date(item.publishTime).toLocaleString('zh-CN') + }; + }); + + return hotNewsList; +} + +async function getZhihuList(): Promise { + const url = `https://api.zhihu.com/topstory/hot-lists/total?limit=50`; + + const { data: ZhihuResponse } = await GET(url); + const zhihuList = ZhihuResponse?.data; + if (!zhihuList) { + return Promise.reject({ + error: 'Failed to get zhihu list' + }); + } + + const hotNewsList: Response[] = zhihuList.map((item) => { + const data = item.target; + return { + title: data.title, + description: data.excerpt, + source: '知乎', + time: new Date(data.created * 1000).toLocaleString('zh-CN') + }; + }); + + return hotNewsList; +} + +async function getWeiboList(): Promise { + const url = + 'https://m.weibo.cn/api/container/getIndex?containerid=106003type%3D25%26t%3D3%26disable_hot%3D1%26filter_type%3Drealtimehot&title=%E5%BE%AE%E5%8D%9A%E7%83%AD%E6%90%9C&extparam=filter_type%3Drealtimehot%26mi_cid%3D100103%26pos%3D0_0%26c_type%3D30%26display_time%3D1540538388&luicode=10000011&lfid=231583'; + + const { data: WeiboResponse } = await GET(url, { + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }); + const weiboList = WeiboResponse.data.cards[0].card_group; + if (!weiboList) { + return Promise.reject({ + error: 'Failed to get weibo list' + }); + } + + const hotNewsList: Response[] = weiboList.map((item) => { + return { + title: item.desc, + source: '微博' + }; + }); + + return hotNewsList; +} + +async function getJuejinList(): Promise { + const url = 'https://api.juejin.cn/content_api/v1/content/article_rank?category_id=1&type=hot'; + + const { data: JuejinResponse } = await GET(url); + const juejinList = JuejinResponse?.data; + if (!juejinList) { + return Promise.reject({ + error: 'Failed to get juejin list' + }); + } + + const hotNewsList: Response[] = juejinList.map((item) => { + return { + title: item.content.title, + source: '掘金' + }; + }); + + return hotNewsList; +} + +async function getToutiaoList(): Promise { + const url = `https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc`; + + const { data: ToutiaoResponse } = await GET(url); + const toutiaoList = ToutiaoResponse?.data; + if (!toutiaoList) { + return Promise.reject({ + error: 'Failed to get toutiao list' + }); + } + const hotNewsList: Response[] = toutiaoList.map((item) => { + return { + title: item.Title, + source: '头条' + }; + }); + + return hotNewsList; +} diff --git a/modules/tool/packages/dailyHot/test/index.test.ts b/modules/tool/packages/dailyHot/test/index.test.ts new file mode 100644 index 00000000..b70e289f --- /dev/null +++ b/modules/tool/packages/dailyHot/test/index.test.ts @@ -0,0 +1,8 @@ +import { expect, test } from 'vitest'; +import tool from '..'; + +test(async () => { + expect(tool.name).toBeDefined(); + expect(tool.description).toBeDefined(); + expect(tool.cb).toBeDefined(); +}); From 77b7a314b4452fa8d9ec9e2ab84f855c5369e592 Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Thu, 16 Oct 2025 21:59:27 +0800 Subject: [PATCH 2/2] perf: code --- modules/tool/packages/dailyHot/src/index.ts | 191 +++++++++----------- 1 file changed, 90 insertions(+), 101 deletions(-) diff --git a/modules/tool/packages/dailyHot/src/index.ts b/modules/tool/packages/dailyHot/src/index.ts index 7c16328b..c8f95c61 100644 --- a/modules/tool/packages/dailyHot/src/index.ts +++ b/modules/tool/packages/dailyHot/src/index.ts @@ -7,23 +7,22 @@ export const InputType = z.object({ export const OutputType = z.object({ hotNewsList: z.array( - z.object({ - title: z.string().describe('hot news title').optional(), - description: z.string().describe('hot news description').optional(), - source: z.string().describe('hot news source website'), - time: z.string().describe('hot news publish time').optional(), - error: z.string().describe('failed to get hot news, error message').optional() - }) + z.union([ + z.object({ + title: z.string().describe('hot news title').optional(), + description: z.string().describe('hot news description').optional(), + source: z.string().describe('hot news source website'), + time: z.string().describe('hot news publish time').optional() + }), + z.object({ + source: z.string().describe('hot news source website'), + error: z.string().describe('failed to get hot news, error message').optional() + }) + ]) ) }); -type Response = { - title?: string; - description?: string; - source: string; - time?: string; - error?: string; -}; +type Response = z.infer['hotNewsList'][number]; type Kr36Response = { data: { @@ -35,93 +34,6 @@ type Kr36Response = { }[]; }; }; - -type ZhihuResponse = { - data: { - target: { - title: string; - created: number; - excerpt: string; - }; - }[]; -}; - -type WeiboResponse = { - data: { - cards: { - card_group?: { - desc: string; - }[]; - }[]; - }; -}; - -type JuejinResponse = { - data: { - content: { - content_id: string; - title: string; - created_at: number; - ctime: number; - }; - author: { - name: string; - }; - content_counter: { - hot_rank: number; - }; - }[]; -}; - -type ToutiaoResponse = { - data: { - Title: string; - }[]; -}; - -export async function tool({ - sources -}: z.infer): Promise> { - const promises = sources.map(async (source) => { - switch (source) { - case '36kr': - return await get36krList(); - case 'zhihu': - return await getZhihuList(); - case 'weibo': - return await getWeiboList(); - case 'juejin': - return await getJuejinList(); - case 'toutiao': - return await getToutiaoList(); - default: - return []; - } - }); - const results = await Promise.allSettled(promises); - const allHotNewsList: Response[] = []; - results.forEach((result, index) => { - if (result.status === 'fulfilled') { - allHotNewsList.push(...result.value); - } else { - allHotNewsList.push({ - error: `Failed to get hot news from ${sources[index]}`, - source: sources[index] - }); - } - }); - - if (!allHotNewsList.length) { - return Promise.reject({ - error: 'Failed to get hot news list' - }); - } - - return { - hotNewsList: allHotNewsList - }; -} - async function get36krList(): Promise { const url = `https://gateway.36kr.com/api/mis/nav/home/nav/rank/hot`; @@ -151,6 +63,15 @@ async function get36krList(): Promise { return hotNewsList; } +type ZhihuResponse = { + data: { + target: { + title: string; + created: number; + excerpt: string; + }; + }[]; +}; async function getZhihuList(): Promise { const url = `https://api.zhihu.com/topstory/hot-lists/total?limit=50`; @@ -175,6 +96,15 @@ async function getZhihuList(): Promise { return hotNewsList; } +type WeiboResponse = { + data: { + cards: { + card_group?: { + desc: string; + }[]; + }[]; + }; +}; async function getWeiboList(): Promise { const url = 'https://m.weibo.cn/api/container/getIndex?containerid=106003type%3D25%26t%3D3%26disable_hot%3D1%26filter_type%3Drealtimehot&title=%E5%BE%AE%E5%8D%9A%E7%83%AD%E6%90%9C&extparam=filter_type%3Drealtimehot%26mi_cid%3D100103%26pos%3D0_0%26c_type%3D30%26display_time%3D1540538388&luicode=10000011&lfid=231583'; @@ -201,6 +131,22 @@ async function getWeiboList(): Promise { return hotNewsList; } +type JuejinResponse = { + data: { + content: { + content_id: string; + title: string; + created_at: number; + ctime: number; + }; + author: { + name: string; + }; + content_counter: { + hot_rank: number; + }; + }[]; +}; async function getJuejinList(): Promise { const url = 'https://api.juejin.cn/content_api/v1/content/article_rank?category_id=1&type=hot'; @@ -222,6 +168,11 @@ async function getJuejinList(): Promise { return hotNewsList; } +type ToutiaoResponse = { + data: { + Title: string; + }[]; +}; async function getToutiaoList(): Promise { const url = `https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc`; @@ -241,3 +192,41 @@ async function getToutiaoList(): Promise { return hotNewsList; } + +export async function tool({ + sources +}: z.infer): Promise> { + const map = { + '36kr': get36krList, + zhihu: getZhihuList, + weibo: getWeiboList, + juejin: getJuejinList, + toutiao: getToutiaoList + }; + const promises = sources.map(async (source) => { + return await map[source](); + }); + + const results = await Promise.allSettled(promises); + const allHotNewsList: Response[] = []; + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + allHotNewsList.push(...result.value); + } else { + allHotNewsList.push({ + error: `Failed to get hot news from ${sources[index]}`, + source: sources[index] + }); + } + }); + + if (!allHotNewsList.length) { + return Promise.reject({ + error: 'Failed to get hot news list' + }); + } + + return { + hotNewsList: allHotNewsList + }; +}