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..c8f95c61 --- /dev/null +++ b/modules/tool/packages/dailyHot/src/index.ts @@ -0,0 +1,232 @@ +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.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 = z.infer['hotNewsList'][number]; + +type Kr36Response = { + data: { + hotRankList: { + templateMaterial: { + widgetTitle: string; + }; + publishTime: string; + }[]; + }; +}; +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; +} + +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`; + + 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; +} + +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'; + + 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; +} + +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'; + + 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; +} + +type ToutiaoResponse = { + data: { + Title: string; + }[]; +}; +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; +} + +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 + }; +} 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(); +});