From c59d8aa2559e9f1ade529088b173ca3cfa2892c5 Mon Sep 17 00:00:00 2001 From: arvinxx Date: Sat, 12 Oct 2024 23:56:42 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20builtin=20search=20wi?= =?UTF-8?q?th=20SearXNG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/tools.ts | 16 + src/libs/trpc/client/index.ts | 1 + src/libs/trpc/client/tools.ts | 13 + src/locales/default/tool.ts | 15 + src/server/modules/SearXNG.ts | 28 + .../tools/__tests__/fixtures/searXNG.ts | 668 ++++++++++++++++++ .../routers/tools/__tests__/search.test.ts | 41 ++ src/server/routers/tools/index.ts | 3 + src/server/routers/tools/search.ts | 20 + src/services/search.ts | 9 + src/store/chat/slices/builtinTool/action.ts | 95 +++ .../chat/slices/builtinTool/initialState.ts | 2 + .../chat/slices/builtinTool/selectors.ts | 3 + src/tools/index.ts | 6 + src/tools/portals.ts | 7 +- src/tools/renders.ts | 3 + .../ResultList/SearchItem/CategoryAvatar.tsx | 70 ++ .../ResultList/SearchItem/TitleExtra.tsx | 38 + .../Portal/ResultList/SearchItem/Video.tsx | 136 ++++ .../Portal/ResultList/SearchItem/index.tsx | 91 +++ .../web-browsing/Portal/ResultList/index.tsx | 21 + src/tools/web-browsing/Portal/index.tsx | 77 ++ .../web-browsing/Render/SearchResultItem.tsx | 64 ++ src/tools/web-browsing/Render/SearchView.tsx | 59 ++ src/tools/web-browsing/Render/ShowMore.tsx | 68 ++ src/tools/web-browsing/Render/index.tsx | 126 ++++ .../web-browsing/components/EngineAvatar.tsx | 32 + .../web-browsing/components/SearchBar.tsx | 134 ++++ src/tools/web-browsing/const.ts | 11 + src/tools/web-browsing/index.ts | 102 +++ src/types/tool/search.ts | 37 + 31 files changed, 1995 insertions(+), 1 deletion(-) create mode 100644 src/config/tools.ts create mode 100644 src/libs/trpc/client/tools.ts create mode 100644 src/server/modules/SearXNG.ts create mode 100644 src/server/routers/tools/__tests__/fixtures/searXNG.ts create mode 100644 src/server/routers/tools/__tests__/search.test.ts create mode 100644 src/server/routers/tools/search.ts create mode 100644 src/services/search.ts create mode 100644 src/tools/web-browsing/Portal/ResultList/SearchItem/CategoryAvatar.tsx create mode 100644 src/tools/web-browsing/Portal/ResultList/SearchItem/TitleExtra.tsx create mode 100644 src/tools/web-browsing/Portal/ResultList/SearchItem/Video.tsx create mode 100644 src/tools/web-browsing/Portal/ResultList/SearchItem/index.tsx create mode 100644 src/tools/web-browsing/Portal/ResultList/index.tsx create mode 100644 src/tools/web-browsing/Portal/index.tsx create mode 100644 src/tools/web-browsing/Render/SearchResultItem.tsx create mode 100644 src/tools/web-browsing/Render/SearchView.tsx create mode 100644 src/tools/web-browsing/Render/ShowMore.tsx create mode 100644 src/tools/web-browsing/Render/index.tsx create mode 100644 src/tools/web-browsing/components/EngineAvatar.tsx create mode 100644 src/tools/web-browsing/components/SearchBar.tsx create mode 100644 src/tools/web-browsing/const.ts create mode 100644 src/tools/web-browsing/index.ts create mode 100644 src/types/tool/search.ts diff --git a/src/config/tools.ts b/src/config/tools.ts new file mode 100644 index 000000000000..d7925a69c8fd --- /dev/null +++ b/src/config/tools.ts @@ -0,0 +1,16 @@ +import { createEnv } from '@t3-oss/env-nextjs'; +import { z } from 'zod'; + +export const getToolsConfig = () => { + return createEnv({ + runtimeEnv: { + SEARXNG_URL: process.env.SEARXNG_URL || 'https://searx.tiekoetter.com/', + }, + + server: { + SEARXNG_URL: z.string().url(), + }, + }); +}; + +export const toolsEnv = getToolsConfig(); diff --git a/src/libs/trpc/client/index.ts b/src/libs/trpc/client/index.ts index 1ff7345e945f..27057b7f53fb 100644 --- a/src/libs/trpc/client/index.ts +++ b/src/libs/trpc/client/index.ts @@ -1,3 +1,4 @@ export { asyncClient } from './async'; export { edgeClient } from './edge'; export * from './lambda'; +export * from './tools'; diff --git a/src/libs/trpc/client/tools.ts b/src/libs/trpc/client/tools.ts new file mode 100644 index 000000000000..5eaa3200a43e --- /dev/null +++ b/src/libs/trpc/client/tools.ts @@ -0,0 +1,13 @@ +import { createTRPCClient, httpBatchLink } from '@trpc/client'; +import superjson from 'superjson'; + +import type { ToolsRouter } from '@/server/routers/tools'; + +export const toolsClient = createTRPCClient({ + links: [ + httpBatchLink({ + transformer: superjson, + url: '/trpc/tools', + }), + ], +}); diff --git a/src/locales/default/tool.ts b/src/locales/default/tool.ts index 5553abf2764f..216a305a019c 100644 --- a/src/locales/default/tool.ts +++ b/src/locales/default/tool.ts @@ -7,4 +7,19 @@ export default { images: '图片:', prompt: '提示词', }, + search: { + createNewSearch: '创建新的搜索记录', + emptyResult: '没有搜索到结果,请修改关键词后重试', + includedTooltip: '当前搜索结果会进入会话的上下文中', + keywords: '关键词:', + scoreTooltip: '相关性分数,该分数越高说明与查询关键词越相关', + searchBar: { + button: '搜索', + placeholder: '关键词', + tooltip: '将会重新获取搜索结果,并创建一条新的总结消息', + }, + searchEngine: '搜索引擎:', + searchResult: '搜索数量:', + viewMoreResults: '查看更多 {{results}} 个结果', + }, }; diff --git a/src/server/modules/SearXNG.ts b/src/server/modules/SearXNG.ts new file mode 100644 index 000000000000..da46dad625d9 --- /dev/null +++ b/src/server/modules/SearXNG.ts @@ -0,0 +1,28 @@ +import qs from 'query-string'; + +import { SearchResponse } from '@/types/tool/search'; + +export class SearXNGClient { + private baseUrl: string; + + constructor(baseUrl: string) { + this.baseUrl = baseUrl; + } + + async search(query: string, engines?: string[]): Promise { + try { + const searchParams = qs.stringify({ + engines: engines?.join(','), + format: 'json', + q: query, + }); + + const response = await fetch(`${this.baseUrl}/search?${searchParams}`); + + return await response.json(); + } catch (error) { + console.error('Error searching:', error); + throw error; + } + } +} diff --git a/src/server/routers/tools/__tests__/fixtures/searXNG.ts b/src/server/routers/tools/__tests__/fixtures/searXNG.ts new file mode 100644 index 000000000000..720cee7a1d03 --- /dev/null +++ b/src/server/routers/tools/__tests__/fixtures/searXNG.ts @@ -0,0 +1,668 @@ +import { SearchResponse } from '@/types/tool/search'; + +export const hetongxue: SearchResponse = { + answers: [ + '老师好我叫何同学. 目标是做有意思的视频 合作请联系:xhaxx1123@163.com. 【何同学】我拍了一张600万人的合影...共计2条视频,包括:正片、教程等,UP主更多精彩视频,请关注UP账号。.', + ], + corrections: [], + infoboxes: [], + number_of_results: 0, + query: '何同学的视频', + results: [ + { + category: 'general', + content: + '老师好我叫何同学. 目标是做有意思的视频 合作请联系:xhaxx1123@163.com. 【何同学】我拍了一张600万人的合影...共计2条视频,包括:正片、教程等,UP主更多精彩视频,请关注UP账号。.', + engine: 'qwant', + engines: ['qwant', 'duckduckgo'], + parsed_url: ['https', 'www.bilibili.com', '/video/BV1Nt4y1D7pW/', '', '', ''], + positions: [1, 1], + score: 4, + template: 'default.html', + title: '【何同学】我拍了一张600万人的合影..._哔哩哔哩_bilibili', + url: 'https://www.bilibili.com/video/BV1Nt4y1D7pW/', + }, + { + category: 'general', + content: + '日常. 老师好我叫何同学. 目标是做有意思的视频 合作请联系:xhaxx1123@163.com. bilibili入站必刷 (17/54) 镇站之宝大集合!. 经典永流传~. BGM:La Ciudad——ODESZA Rap God—— Eminem Stronger —— Kanye West 感谢龚哥从上海帮我办了两次电话卡,感谢我班团支书辅导我数电实验 ...', + engine: 'qwant', + engines: ['qwant', 'duckduckgo'], + parsed_url: ['https', 'www.bilibili.com', '/video/BV1f4411M7QC/', '', '', ''], + positions: [2, 2], + score: 2, + template: 'default.html', + title: '【何同学】有多快?5G在日常使用中的真实体验_哔哩哔哩_bilibili', + url: 'https://www.bilibili.com/video/BV1f4411M7QC/', + }, + { + category: 'general', + content: + '听歌学习. 老师好我叫何同学. 目标是做有意思的视频 合作请联系:xhaxx1123@163.com. 视频中4分30秒便签纸上的想法来源于弱智吧。感谢所有参加我们这个测试的朋友,更感谢大家的一键三连!, 视频播放量 7509667、弹幕量 21154、点赞数 961103、投硬币枚数 518679、收藏 ...', + engine: 'qwant', + engines: ['qwant', 'duckduckgo'], + parsed_url: [ + 'https', + 'www.bilibili.com', + '/video/BV1fu411G7e3/', + '', + 'buvid=Y04D7200119A527544F7BD05A4D5EFFB6995&is_story_h5=false&mid=FMYupz80zPVDoNSZftV%2F8w%3D%3D&p=1&plat_id=168&share_from=ugc&share_plat=ios&share_session_id=93B4EE75-B04C-4C94-A117-22DDD0F36313&share_tag=s_i&unique_k=5HuqFvO&up_id=163637592', + '', + ], + positions: [3, 3], + score: 1.333_333_333_333_333_3, + template: 'default.html', + title: '【何同学】 为了找到专注的秘诀,我们找500人做了个实验 ...', + url: 'https://www.bilibili.com/video/BV1fu411G7e3/?buvid=Y04D7200119A527544F7BD05A4D5EFFB6995&is_story_h5=false&mid=FMYupz80zPVDoNSZftV%2F8w%3D%3D&p=1&plat_id=168&share_from=ugc&share_plat=ios&share_session_id=93B4EE75-B04C-4C94-A117-22DDD0F36313&share_tag=s_i&unique_k=5HuqFvO&up_id=163637592', + }, + { + category: 'general', + content: '目標是做有意思的視頻!商務合作:xhaxx1123@163.com', + engine: 'qwant', + engines: ['qwant', 'duckduckgo'], + parsed_url: ['https', 'www.youtube.com', '/c/hetongxue', '', '', ''], + positions: [4, 5], + score: 0.9, + template: 'default.html', + title: '老师好我叫何同学 - YouTube', + url: 'https://www.youtube.com/c/hetongxue', + }, + { + category: 'general', + content: + '2018年,何同学上传了18条视频,最忠实的一位粉丝总共看了545次,平均每条观看30.2次。 那是他的小号,登录在妈妈的手机上。 他在B站和微博上感谢了妈妈,但在现实生活中,他们从来没有聊起过这件事。', + engine: 'qwant', + engines: ['qwant', 'duckduckgo'], + parsed_url: [ + 'https', + 'baike.baidu.com', + '/item/%E8%80%81%E5%B8%88%E5%A5%BD%E6%88%91%E5%8F%AB%E4%BD%95%E5%90%8C%E5%AD%A6/55515999', + '', + '', + '', + ], + positions: [5, 6], + score: 0.733_333_333_333_333_4, + template: 'default.html', + title: '老师好我叫何同学 - 百度百科', + url: 'https://baike.baidu.com/item/%E8%80%81%E5%B8%88%E5%A5%BD%E6%88%91%E5%8F%AB%E4%BD%95%E5%90%8C%E5%AD%A6/55515999', + }, + { + category: 'general', + content: + '【何同学Vlog】为什么一期视频做了325天... 在我们流水线视频长达325天的制作周期里,工作室的小伙伴记录了很多有趣的素材,这期幕后视频就给大家看看我们都翻过什么车…结尾还有一个小惊喜给大家!', + engine: 'qwant', + engines: ['qwant', 'duckduckgo'], + parsed_url: ['https', 'www.douyin.com', '/video/7331651006691937577', '', '', ''], + positions: [6, 7], + score: 0.619_047_619_047_619_1, + template: 'default.html', + title: '【何同学Vlog】为什么一期视频做了325天... 在我们流水线 ...', + url: 'https://www.douyin.com/video/7331651006691937577', + }, + { + category: 'general', + content: + '老师好我叫何同学(1999年 —,简称何同学),本名何世杰 [1],是中国大陆的网络视频制作者。 何同学从2017年开始在 bilibili 等网站发布自制的科技视频,此后逐渐得到关注;2019年发布评测 5G网络 的视频后走红,获得 bilibili 2020年度最佳优秀奖作品UP主奖项,2021 ...', + engine: 'qwant', + engines: ['qwant', 'duckduckgo'], + parsed_url: [ + 'https', + 'zh.wikipedia.org', + '/wiki/%E8%80%81%E5%B8%88%E5%A5%BD%E6%88%91%E5%8F%AB%E4%BD%95%E5%90%8C%E5%AD%A6', + '', + '', + '', + ], + positions: [7, 9], + score: 0.507_936_507_936_507_9, + template: 'default.html', + title: '老师好我叫何同学 - 维基百科,自由的百科全书', + url: 'https://zh.wikipedia.org/wiki/%E8%80%81%E5%B8%88%E5%A5%BD%E6%88%91%E5%8F%AB%E4%BD%95%E5%90%8C%E5%AD%A6', + }, + { + category: 'general', + content: + '何同学至今发布的42条视频中,大多数为数码产品的测评,而有些视频中,他向大家展示了真实生活中的何同学。 “颓废的十七岁”“我发现我无法去做任何一件想做的事情,这实在是太悲伤了”“说真的我想当个艺术家”。', + engine: 'qwant', + engines: ['qwant', 'duckduckgo'], + parsed_url: ['https', 'new.qq.com', '/rain/a/20210805A07ID500', '', '', ''], + positions: [9, 10], + score: 0.422_222_222_222_222_2, + template: 'default.html', + title: '“何同学”是谁:用镜头讲述理科生的浪漫,毕业视频登上热搜 ...', + url: 'https://new.qq.com/rain/a/20210805A07ID500', + }, + { + category: 'general', + content: + '如今,迟到一个月的毕业视频已经面世,成为何同学告别校园的句号。发布三天后,视频微博点赞超135万,B站播放量达到650万。同一时间,他的对外微信昵称,再次改成了“小何在准备新视频了”。', + engine: 'qwant', + engines: ['qwant', 'duckduckgo'], + parsed_url: ['https', 'new.qq.com', '/rain/a/20210730A062Y300', '', '', ''], + positions: [10, 13], + score: 0.353_846_153_846_153_87, + template: 'default.html', + title: '何同学:属于我的15分钟已经过去了 - 腾讯网', + url: 'https://new.qq.com/rain/a/20210730A062Y300', + }, + { + category: 'general', + content: + '哔哩哔哩老师好我叫何同学的个人空间,提供老师好我叫何同学分享的视频、音频、文章、动态、收藏等内容,关注老师好我叫何同学账号,第一时间了解UP注动态。', + engine: 'brave', + engines: ['brave', 'google'], + parsed_url: ['https', 'space.bilibili.com', '/163637592/', '', '', ''], + positions: [3, 1], + publishedDate: null, + score: 2.666_666_666_666_666_5, + template: 'default.html', + thumbnail: null, + title: '老师好我叫何同学的个人空间-老师好我叫何同学个人主页-哔哩哔哩视频', + url: 'https://space.bilibili.com/163637592/', + }, + { + category: 'general', + content: + '何同学做的桌子,图/B站视频截图. 接下来的7分钟里,何同学详细展示了这款桌子的构思、制作过程。 除了超强的动手能力和巧妙的构思之外,何同学的聪明之处在于,他是一个会讲故事的人。 观众的情绪是在一点点被抬高的,直到成品做出达到顶峰。 开头只是一个会移动的水杯,然后更多炫酷的功能逐渐叠加,从被苹果放弃的产品无线充电板AirPower,到整个桌面可以实现充电,能够显示“喝水提醒”、“日程”,最后出现两个红色的大字“下班”,像是一场发布会的谢幕,最精彩之处戛然而止。 何同学无疑才华横溢,但或许更应该感慨,我们这个时代的媒介正在发生彻底的变化,是多方面的因素,推动着何同学向前。 01 何同学是怎么火起来的? 回答这个问题,核心还是在本文开头所提及的: 讲故事的能力。', + engine: 'brave', + engines: ['qwant', 'brave', 'duckduckgo'], + parsed_url: ['https', 'new.qq.com', '/rain/a/20211202A05C1100', '', '', ''], + positions: [10, 8, 8], + publishedDate: null, + score: 1.05, + template: 'default.html', + thumbnail: '', + title: '何同学爆火背后的深层原因_腾讯新闻', + url: 'https://new.qq.com/rain/a/20211202A05C1100', + }, + { + category: 'general', + content: + 'January 15, 2024 - 老师好我叫何同学(1999年—,简称何同学),本名何世杰,是中国大陆的网络视频制作者。何同学从2017年开始在bilibili等网站发布自制的科技视频,此后逐渐得到关注;2019年发布评测5G网络的视频后走红,获得bilibili2020年...', + engine: 'brave', + engines: ['brave'], + parsed_url: ['https', 'zh.m.wikipedia.org', '/wiki/何世杰', '', '', ''], + positions: [1], + publishedDate: '2024-01-15T00:00:00', + score: 1, + template: 'default.html', + thumbnail: '', + title: '老师好我叫何同学 - 维基百科,自由的百科全书', + url: 'https://zh.m.wikipedia.org/wiki/何世杰', + }, + { + category: 'general', + content: + '哔哩哔哩何同学工作室的个人空间,提供何同学工作室分享的视频、音频、文章、动态、收藏等内容,关注何同学工作室账号,第一时间了解UP注动态。老师好我叫何同学工作室 ...', + engine: 'brave', + engines: ['brave', 'google'], + parsed_url: ['https', 'space.bilibili.com', '/1192648858/', '', '', ''], + positions: [12, 4], + publishedDate: null, + score: 0.666_666_666_666_666_6, + template: 'default.html', + thumbnail: null, + title: '何同学工作室的个人空间-何同学工作室个人主页-哔哩哔哩视频', + url: 'https://space.bilibili.com/1192648858/', + }, + { + category: 'general', + content: + '再看了一下,确实不是数码区的视频,但何同学的确是在数码区讲故事的Up主,无论是哪个区,何同学的视频都有"故事感人"和"深度不足"的两个特点,其实第1P的内容与热度完全不及第2P与库克的对话,即便那可能只是个恰饭。', + engine: 'brave', + engines: ['brave', 'duckduckgo'], + parsed_url: ['https', 'www.zhihu.com', '/question/444922035', '', '', ''], + positions: [4, 14], + publishedDate: null, + score: 0.642_857_142_857_142_8, + template: 'default.html', + thumbnail: '', + title: '如何评价何同学视频「【何同学】整理自己的生活」?有哪些亮点和不足? - 知乎', + url: 'https://www.zhihu.com/question/444922035', + }, + { + category: 'general', + content: + '7月25日,B站up主“老师好我叫何同学”(以下简称“何同学”)发布了最新一期视频《我毕业了!!》当晚播放量超200万,同时登上热搜榜一,万千人一起讨论这个大学本科毕业的学生。 · 不得不说,这个视频彻底爆了。...', + engine: 'brave', + engines: ['brave'], + parsed_url: ['https', 'zhuanlan.zhihu.com', '/p/394479680', '', '', ''], + positions: [2], + publishedDate: null, + score: 0.5, + template: 'default.html', + thumbnail: '', + title: '看完何同学的视频,谈谈我的启发 - 知乎', + url: 'https://zhuanlan.zhihu.com/p/394479680', + }, + { + category: 'general', + content: + '老师好我叫何同学 · 今年WWDC最有用的十個新功能 · 這是我見過最精緻的電子產品...何同學工作室5月開箱 · 【何同學】它不是電腦。M4 iPad Pro深度體驗 · 新iPad Pro現場上手!', + engine: 'google', + engines: ['google'], + parsed_url: [ + 'https', + 'www.youtube.com', + '/channel/UCP5Kd0smdWe9H_bDH-73c6Q/videos', + '', + '', + '', + ], + positions: [2], + score: 0.5, + template: 'default.html', + thumbnail: null, + title: '老师好我叫何同学', + url: 'https://www.youtube.com/channel/UCP5Kd0smdWe9H_bDH-73c6Q/videos', + }, + { + category: 'general', + content: + '老师好我叫何同学 · 今年WWDC最有用的十個新功能 · 這是我見過最精緻的電子產品...何同學工作室5月開箱 · 【何同學】它不是電腦。M4 iPad Pro深度體驗 · 新iPad Pro現場上手!', + engine: 'google', + engines: ['google'], + parsed_url: ['https', 'www.youtube.com', '/c/hetongxue/videos', '', '', ''], + positions: [3], + score: 0.333_333_333_333_333_3, + template: 'default.html', + thumbnail: null, + title: '老师好我叫何同学', + url: 'https://www.youtube.com/c/hetongxue/videos', + }, + { + category: 'general', + content: + 'November 2, 2021 - 价格战中的合资车企,可能活不到双11了 · 多产品亚马逊类目第一,老牌家纺品牌年营收超5亿美金|Insight 全球', + engine: 'brave', + engines: ['brave', 'duckduckgo'], + parsed_url: ['https', 'www.36kr.com', '/p/1467640415030021', '', '', ''], + positions: [13, 16], + publishedDate: '2021-11-02T00:00:00', + score: 0.278_846_153_846_153_85, + template: 'default.html', + thumbnail: '', + title: '一条视频带火上市公司,何同学凭什么成为顶流大V?-36氪', + url: 'https://www.36kr.com/p/1467640415030021', + }, + { + category: 'general', + content: + '你眼里的光. 老师好我叫何同学. 目标是做有意思的视频 合作请联系:xhaxx1123@163.com. 制片:九老师翻页书:Flashcat闪电猫翻页书动画:超级小树动画:Joyteeth 小步乱撞音效:Jimmy感谢巡天者叶梓颐 狂奔的茄子指导我星空摄影 感谢SmallRig帮我打不锈钢云台 感谢俺的 ...', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: ['https', 'www.bilibili.com', '/video/BV1764y167Lp/', '', '', ''], + positions: [4], + score: 0.25, + template: 'default.html', + title: '【何同学】永远是同学_哔哩哔哩_bilibili', + url: 'https://www.bilibili.com/video/BV1764y167Lp/', + }, + { + category: 'general', + content: + '新老演员同台较量,共同见证新一届"脱口秀大王"的诞生。 【何同学纯享】数码视频界天花板现场后空翻,节目简介:《脱口秀大会》第五季是一档原创棚内喜剧脱口秀竞演节目。', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: ['https', 'v.qq.com', '/x/cover/mzc0020016b6v6q/r004495ki0j.html', '', '', ''], + positions: [11], + score: 0.090_909_090_909_090_91, + template: 'default.html', + title: '【何同学纯享】数码视频界天花板现场后空翻_综艺_高清完整 ...', + url: 'https://v.qq.com/x/cover/mzc0020016b6v6q/r004495ki0j.html', + }, + { + category: 'general', + content: + '家境一般的观众看了会感觉讲了又好像没讲,生活环境不是一个阶级的。. 但对于甲方来说,视频保持了何同学一贯的风格,讲的故事符合甲方心中的美好画面。. 编辑于 2023-10-20 04:16. 可可西里. 性情凉薄 寡恩薄义. 我的评价:和孙国帅没差. 发布于 2023-10 ...', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: ['https', 'www.zhihu.com', '/question/626120311', '', '', ''], + positions: [12], + score: 0.083_333_333_333_333_33, + template: 'default.html', + title: '如何评价何同学的新视频《鸿蒙的最后一块拼图 | Mate60 ...', + url: 'https://www.zhihu.com/question/626120311', + }, + { + category: 'general', + content: + '本地视频 播放 桌面便捷访问 立即体验客户端 续费 开通电视特权 个人中心 我的加追 ... 【何同学】和苹果CEO库克的 采访 2021年2月19日发布 18:14 【何同学】和苹果CEO库克的采访 ...', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: ['https', 'v.qq.com', '/x/page/a3228iquze8.html', '', '', ''], + positions: [15], + score: 0.066_666_666_666_666_67, + template: 'default.html', + title: '【何同学】和苹果CEO库克的采访_腾讯视频', + url: 'https://v.qq.com/x/page/a3228iquze8.html', + }, + { + category: 'general', + content: + '哔哩哔哩老师好我叫何同学的个人空间,提供老师好我叫何同学分享的视频、音频、文章、动态、收藏等内容,关注老师好我叫何同学账号,第一时间了解UP主动态。.', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: [ + 'https', + 'space.bilibili.com', + '/163637592/channel/collectiondetail', + '', + 'sid=2159893', + '', + ], + positions: [17], + score: 0.058_823_529_411_764_705, + template: 'default.html', + title: '老师好我叫何同学的个人空间-老师好我叫何同学个人主页-哔 ...', + url: 'https://space.bilibili.com/163637592/channel/collectiondetail?sid=2159893', + }, + { + category: 'general', + content: + '转型中的何同学,终于发力短视频. 10月27日,何同学正式进军竖屏短视频。 当晚,时隔两个半月未更新视频的何同学在B站发布了一条9分27秒的新视频"快充伤电池? 40部手机两年试验,告诉你最佳充电方式"。 与此同时, 他在抖音发布了4条1分钟的竖屏短视频,对前面提到的这条新视频的内容进行了拆解和重新剪辑,使其更适配竖屏的形式。...', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: ['https', 'www.36kr.com', '/p/1980755612173318', '', '', ''], + positions: [18], + score: 0.055_555_555_555_555_55, + template: 'default.html', + title: '1天涨粉28万,何同学勇闯抖音-36氪', + url: 'https://www.36kr.com/p/1980755612173318', + }, + { + category: 'general', + content: + '7月25日,B站up主"老师好我叫何同学"(以下简称"何同学")发布了最新一期视频《我毕业了! 》当晚播放量超200万,同时登上热搜榜一,万千人一起讨论这个大学本科毕业的学生。', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: ['https', 'www.zhihu.com', '/tardis/zm/art/394479680', '', '', ''], + positions: [19], + score: 0.052_631_578_947_368_42, + template: 'default.html', + title: '看完何同学的视频,谈谈我的启发', + url: 'https://www.zhihu.com/tardis/zm/art/394479680', + }, + { + category: 'general', + content: + '近日,微博大V @老师好我叫何同学 发布了一条数码科普类的视频,《我做了苹果放弃的产品……》。. 该视频很快在微博和B站传播开来,登上微博热搜,刷爆全网。. 流量暴增的背后,其广告主乐歌股份则成为最大的赢家——视频发布第二天,产品所属上市公司 ...', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: ['https', 'www.thepaper.cn', '/newsDetail_forward_15024428', '', '', ''], + positions: [20], + score: 0.05, + template: 'default.html', + title: '"何同学"商业价值背后,是中视频卡位战_澎湃号·湃客_澎湃 ...', + url: 'https://www.thepaper.cn/newsDetail_forward_15024428', + }, + { + category: 'general', + content: + '总之,何同学在这期视频中熟练地运用了苏秦张仪表演法,把流传的专注方法纵横整合起来做了个科学性看似很高,实则漏洞百出(其实就是很能唬人)的所谓的实验,得出的结论几乎都是废话或者是已经人尽皆知,没有产生任何新观点。', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: ['https', 'www.zhihu.com', '/question/614636330', '', '', ''], + positions: [21], + score: 0.047_619_047_619_047_616, + template: 'default.html', + title: '如何评价何同学新视频《为了找到专注的秘诀,我们找500人 ...', + url: 'https://www.zhihu.com/question/614636330', + }, + { + category: 'general', + content: + '2019年6月6月19:48,@老师好我叫何同学 发布了一则5G的评测视频,截至6月11日,18w余次转发,4w余次评论,27w余次的点赞,OPPO副总裁抛来橄榄枝,各大媒体争相报道……这位来自北京邮电大学的小伙子,在互联网上彻彻底底...', + engine: 'brave', + engines: ['brave'], + parsed_url: ['https', 'm.thepaper.cn', '/newsDetail_forward_3659739', '', '', ''], + positions: [5], + publishedDate: null, + score: 0.2, + template: 'default.html', + thumbnail: '', + title: '“何同学”走向人前的背后:谁是18w分之一的传播助力者?', + url: 'https://m.thepaper.cn/newsDetail_forward_3659739', + }, + { + category: 'general', + content: + '作为一名数码区视频博主,何同学发布的内容大部分为数码测评视频,但有别于其他严肃专业的测评视频,何同学的视频特点并不在于信息增量上的突出,没有性能参数,而是以新颖的想法和流畅的剪辑,从自身体验和人...', + engine: 'brave', + engines: ['brave'], + parsed_url: ['https', 'm.mp.oeeee.com', '/a/BAAFRD000020210805529530.html', '', '', ''], + positions: [6], + publishedDate: null, + score: 0.166_666_666_666_666_66, + template: 'default.html', + thumbnail: '', + title: '“何同学”是谁:用镜头讲述理科生的浪漫,毕业视频登上热搜', + url: 'https://m.mp.oeeee.com/a/BAAFRD000020210805529530.html', + }, + { + category: 'general', + content: + "放假会做贼有意思的视频!工作事宜联系:xhaxx1123@163.com I'll make creative, funny, interesting videos while vacation. Business cooperation: xhaxx1123@163.com", + engine: 'brave', + engines: ['brave'], + parsed_url: ['https', 'www.youtube.com', '/@hetongxue', '', '', ''], + positions: [7], + publishedDate: null, + score: 0.142_857_142_857_142_85, + template: 'default.html', + thumbnail: '', + title: '老师好我叫何同学', + url: 'https://www.youtube.com/@hetongxue', + }, + { + category: 'general', + content: + '我个人主观评价一下 · 首先我本人很早就看过何同学视频,但是一直没关注,也就是首页偶尔看看。2022他的视频给我感觉就是吃老本,因为无论产量、内容深度、视频精致度都很一般,真的很一般。作为通信行业从业者和...', + engine: 'brave', + engines: ['brave'], + parsed_url: ['https', 'www.zhihu.com', '/question/578552358/answer/2849217080', '', '', ''], + positions: [8], + publishedDate: null, + score: 0.125, + template: 'default.html', + thumbnail: '', + title: '如何评价何同学无缘2022年百大up主? - 知乎', + url: 'https://www.zhihu.com/question/578552358/answer/2849217080', + }, + { + category: 'general', + content: + 'August 5, 2020 - 同为B站科技区UP主,何同学的视频内容和大部分同类型UP主不一样。科技区相当一部分视频是严肃专业的测评,但作为B站最著名的科技UP主之一,何同学却从未发过一个纯粹意义上的专业测评。他上初中开始看手机测评,“...', + engine: 'brave', + engines: ['brave'], + parsed_url: [ + 'https', + 'tech.sina.cn', + '/i/gn/2020-08-05/detail-iivhvpwx9366000.d.html', + '', + '', + '', + ], + positions: [9], + publishedDate: '2020-08-05T00:00:00', + score: 0.111_111_111_111_111_1, + template: 'default.html', + thumbnail: '', + title: '老师好我叫何同学,我有些不开心_手机新浪网', + url: 'https://tech.sina.cn/i/gn/2020-08-05/detail-iivhvpwx9366000.d.html', + }, + { + category: 'general', + content: + '何同学的视频相比其他,更多的是心意和新意。他每一个镜 头的运用、转场真的是能够看出用心的,其实对我比较印象 深刻的还是 何同学有着与他身份不相符的能力。', + engine: 'brave', + engines: ['brave'], + parsed_url: [ + 'https', + 'wizardforcel.gitbooks.io', + '/jiuliaozhengqian/content/022.html', + '', + '', + '', + ], + positions: [11], + publishedDate: null, + score: 0.090_909_090_909_090_91, + template: 'default.html', + thumbnail: '', + title: '简单说说「何同学」 · 就聊挣钱知识星球精华', + url: 'https://wizardforcel.gitbooks.io/jiuliaozhengqian/content/022.html', + }, + { + category: 'general', + content: + '7月25日,“何同学毕业”突然上了热搜,还占据娱乐榜首位,网友纳闷了:何同学是谁啊?他毕业有必要上热搜吗?紧接着就迎来了网友的不满。 · 简单介绍一下,何同学的网名叫“老师好我叫何同学”,本名叫何世杰,...', + engine: 'brave', + engines: ['brave'], + parsed_url: ['https', 'zhuanlan.zhihu.com', '/p/393257035', '', '', ''], + positions: [14], + publishedDate: null, + score: 0.071_428_571_428_571_42, + template: 'default.html', + thumbnail: '', + title: '何同学是谁?为什么他毕业能上热搜,又为什么引来网友非议? - 知乎', + url: 'https://zhuanlan.zhihu.com/p/393257035', + }, + { + category: 'general', + content: + 'February 3, 2022 - □袁珮芸(西南大学) · 有着B站顶级“鸽王”称号的UP主老师好我叫何同学(以下简称“何同学”),2月2日发布了一期题为“我用108天开了个灯……”的视频。截止至2月3日13:27分,视频获得291.7万播放量,55.7万点赞,位...', + engine: 'brave', + engines: ['brave'], + parsed_url: ['https', 'moment.rednet.cn', '/content/2022/02/03/10840348.html', '', '', ''], + positions: [15], + publishedDate: '2022-02-03T00:00:00', + score: 0.066_666_666_666_666_67, + template: 'default.html', + thumbnail: '', + title: '何同学的视频,让人们看到了“科技的浪漫”', + url: 'https://moment.rednet.cn/content/2022/02/03/10840348.html', + }, + { + category: 'general', + content: + '何同学是知名的鸽子。. 上上上次更新,是2021年7月,此时的他正忙完自己的毕业设计兼毕业庆祝视频---用树莓派3D Lidar扫描仪拍摄的一段星轨视频;. 上上次更新,则是2021年10月,利用毕业后的两个月的时间里,他创办了自己的工作室,并做出了苹果放弃的产品 ...', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: ['https', 'baijiahao.baidu.com', '/s', '', 'id=1730443414809360080', ''], + positions: [22], + score: 0.045_454_545_454_545_456, + template: 'default.html', + title: '被嘲讽成"赛博丁真"数十天后,何同学用新视频,进行最强回击', + url: 'https://baijiahao.baidu.com/s?id=1730443414809360080', + }, + { + category: 'general', + content: + '为此,人力资源学院于5月14日下午3点成功举办了第二届"火苗"新媒体训练营——视频剪辑培训,为热爱视频创作的同学们搭建了一个学习与交流的平台。. 此次培训课程内容丰富且实用,重点围绕视频剪辑软件Pr的基础操作和高级技巧展开。. 助教廖峻贤首先 ...', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: ['https', 'rlzy.gdufe.edu.cn', '/2024/0603/c7288a193155/page.htm', '', '', ''], + positions: [23], + score: 0.043_478_260_869_565_216, + template: 'default.html', + title: '人力资源学院第二届火苗新媒体训练营 视频剪辑培训成功举办', + url: 'https://rlzy.gdufe.edu.cn/2024/0603/c7288a193155/page.htm', + }, + { + category: 'general', + content: + '【何同学】我用一万行备忘录做了个动画...共计2条视频,包括:正片、54秒动画(60帧数)等,UP主更多精彩视频,请关注UP账号。', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: ['https', 'www.bilibili.com', '/video/BV1Xi4y1x7eM/', '', '', ''], + positions: [24], + score: 0.041_666_666_666_666_664, + template: 'default.html', + title: '【何同学】我用一万行备忘录做了个动画..._哔哩哔哩_bilibili', + url: 'https://www.bilibili.com/video/BV1Xi4y1x7eM/', + }, + { + category: 'general', + content: + '6月1日,"珠峰杯"第四届中国大学生韩国语短视频大赛颁奖典礼在浙江杭州举行。亚洲学院2021级朝鲜语专业本科生何柔亿、2022级朝鲜语专业本科生屈京京两位同学在本次大赛中提交的作品《交流,和而不同之路》在本次比赛中荣获全国优秀奖。 何柔亿、屈京京同学在颁奖典礼现场', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: [ + 'https', + 'chaoyu.bisu.edu.cn', + '/art/2024/6/8/art_10721_331167.html', + '', + '', + '', + ], + positions: [25], + score: 0.04, + template: 'default.html', + title: '人才培养|亚洲学院学生在"珠峰杯"第四届中国大学生韩国语 ...', + url: 'https://chaoyu.bisu.edu.cn/art/2024/6/8/art_10721_331167.html', + }, + { + category: 'general', + content: + '通知公告. 关于中国农业大学第六届大学生思政课微视频大赛评选结果的公示. 发布日期:2024-06-24浏览次数: 信息来源:马克思主义学院. 2024年是中华人民共和国成立75周年,也是实现"十四五"规划目标任务的关键一年。. 为坚持不懈用习近平新时代中国特色 ...', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: ['https', 'cm.cau.edu.cn', '/art/2024/6/24/art_25483_1030364.html', '', '', ''], + positions: [26], + score: 0.038_461_538_461_538_464, + template: 'default.html', + title: '关于中国农业大学第六届大学生思政课微视频大赛评选结果 ...', + url: 'https://cm.cau.edu.cn/art/2024/6/24/art_25483_1030364.html', + }, + { + category: 'general', + content: + '6月21日上午,前沿科学技术研究院在中国西部科技创新港举行2024届毕业生欢送会,为即将奔赴人生新旅程的毕业生献上诚挚的祝福。前沿院党委书记赵卫滨、前沿院院长赵永席、副院长何刚、副院长邵永平、前沿院党委副书记李妙辉和教师代表,以及2024届毕业生及家属参加欢送会,会议由学院辅导 ...', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: ['https', 'fist.xjtu.edu.cn', '/info/1007/4104.htm', '', '', ''], + positions: [27], + score: 0.037_037_037_037_037_035, + template: 'default.html', + title: '以青春之名,赴时代之约——前沿院2024届毕业生欢送会举办 ...', + url: 'https://fist.xjtu.edu.cn/info/1007/4104.htm', + }, + { + category: 'general', + content: + '《脱口秀大会第5季》第9期上:半决赛开启!苏醒爆笑开场,节目简介:《脱口秀大会》第五季是一档原创棚内喜剧脱口秀竞演节目。来自各行各业的脱口秀演员根据对生活的观察和感悟,以不同的视角切入,通过专业的喜剧创作能力,向观众展示一场场高质量的脱口秀表演,并且传递幽默的魅力 ...', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: ['https', 'v.qq.com', '/x/cover/mzc0020016b6v6q/b0044krsgv5.html', '', '', ''], + positions: [28], + score: 0.035_714_285_714_285_71, + template: 'default.html', + title: '脱口秀大会第5季 - 腾讯视频', + url: 'https://v.qq.com/x/cover/mzc0020016b6v6q/b0044krsgv5.html', + }, + { + category: 'general', + content: + '财政部拟发行2024年记账式贴现(三十三期)国债(91天)。. 现就本次发行工作有关事宜通知如下:. 一、债券要素. (一)品种。. 本期国债为期限91天的贴现债。. (二)发行数量。. 本期国债竞争性招标面值总额400亿元,进行甲类成员追加投标。. (三)发行 ...', + engine: 'duckduckgo', + engines: ['duckduckgo'], + parsed_url: [ + 'http', + 'www.gks.mof.gov.cn', + '/ztztz/guozaiguanli/gzfxzjs/202406/t20240606_3936684.htm', + '', + '', + '', + ], + positions: [29], + score: 0.034_482_758_620_689_655, + template: 'default.html', + title: '关于2024年记账式贴现(三十三期)国债发行工作有关事宜 ...', + url: 'http://www.gks.mof.gov.cn/ztztz/guozaiguanli/gzfxzjs/202406/t20240606_3936684.htm', + }, + ], + suggestions: [], + unresponsive_engines: [], +}; diff --git a/src/server/routers/tools/__tests__/search.test.ts b/src/server/routers/tools/__tests__/search.test.ts new file mode 100644 index 000000000000..36aa2e31eba9 --- /dev/null +++ b/src/server/routers/tools/__tests__/search.test.ts @@ -0,0 +1,41 @@ +// @vitest-environment node +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { toolsEnv } from '@/config/tools'; + +/** + * This file contains the root router of your tRPC-backend + */ +import { createCallerFactory } from '@/libs/trpc'; +import { AuthContext, createContextInner } from '@/server/context'; +import { SearXNGClient } from '@/server/modules/SearXNG'; + +import { searchRouter } from '../search'; +import { hetongxue } from './fixtures/searXNG'; + +vi.mock('@/config/tools', () => ({ + toolsEnv: vi.fn(), +})); + +const createCaller = createCallerFactory(searchRouter); +let ctx: AuthContext; +let router: ReturnType; + +beforeEach(async () => { + vi.resetAllMocks(); + ctx = await createContextInner({ userId: 'mock' }); + router = createCaller(ctx); +}); + +describe('searchRouter', () => { + describe('search', () => { + it('搜索结果超过10个', async () => { + vi.spyOn(SearXNGClient.prototype, 'search').mockResolvedValueOnce(hetongxue); + + const results = await router.query({ query: '何同学' }); + + // Assert + expect(results.results.length).toEqual(10); + }); + }); +}); diff --git a/src/server/routers/tools/index.ts b/src/server/routers/tools/index.ts index 3ee6f23a8dc7..2da5b282111c 100644 --- a/src/server/routers/tools/index.ts +++ b/src/server/routers/tools/index.ts @@ -1,7 +1,10 @@ import { publicProcedure, router } from '@/libs/trpc'; +import { searchRouter } from './search'; + export const toolsRouter = router({ healthcheck: publicProcedure.query(() => "i'm live!"), + search: searchRouter, }); export type ToolsRouter = typeof toolsRouter; diff --git a/src/server/routers/tools/search.ts b/src/server/routers/tools/search.ts new file mode 100644 index 000000000000..71c6286d73e3 --- /dev/null +++ b/src/server/routers/tools/search.ts @@ -0,0 +1,20 @@ +import { z } from 'zod'; + +import { toolsEnv } from '@/config/tools'; +import { authedProcedure, router } from '@/libs/trpc'; +import { SearXNGClient } from '@/server/modules/SearXNG'; + +export const searchRouter = router({ + query: authedProcedure + .input( + z.object({ + query: z.string(), + searchEngine: z.array(z.string()).optional(), + }), + ) + .query(async ({ input }) => { + const client = new SearXNGClient(toolsEnv.SEARXNG_URL); + + return await client.search(input.query, input.searchEngine); + }), +}); diff --git a/src/services/search.ts b/src/services/search.ts new file mode 100644 index 000000000000..dd6075691f7a --- /dev/null +++ b/src/services/search.ts @@ -0,0 +1,9 @@ +import { toolsClient } from '@/libs/trpc/client'; + +class SearchService { + search(query: string, searchEngine?: string[]) { + return toolsClient.search.query.query({ query, searchEngine }); + } +} + +export const searchService = new SearchService(); diff --git a/src/store/chat/slices/builtinTool/action.ts b/src/store/chat/slices/builtinTool/action.ts index d6e4ee3e7b75..fd04f1216d38 100644 --- a/src/store/chat/slices/builtinTool/action.ts +++ b/src/store/chat/slices/builtinTool/action.ts @@ -5,13 +5,17 @@ import { StateCreator } from 'zustand/vanilla'; import { useClientDataSWR } from '@/libs/swr'; import { fileService } from '@/services/file'; +import { searchService } from '@/services/search'; import { imageGenerationService } from '@/services/textToImage'; import { uploadService } from '@/services/upload'; import { chatSelectors } from '@/store/chat/selectors'; import { ChatStore } from '@/store/chat/store'; import { useFileStore } from '@/store/file'; +import { CreateMessageParams } from '@/types/message'; import { DallEImageItem } from '@/types/tool/dalle'; +import { SearchContent, SearchQuery } from '@/types/tool/search'; import { setNamespace } from '@/utils/storeDebug'; +import { nanoid } from '@/utils/uuid'; const n = setNamespace('tool'); @@ -21,8 +25,25 @@ const SWR_FETCH_KEY = 'FetchImageItem'; */ export interface ChatBuiltinToolAction { generateImageFromPrompts: (items: DallEImageItem[], id: string) => Promise; + /** + * 重新发起搜索 + * @description 会更新插件的 arguments 参数,然后再次搜索 + */ + reSearchWithSearXNG: ( + id: string, + data: SearchQuery, + options?: { aiSummary: boolean }, + ) => Promise; + saveSearXNGSearchResult: (id: string) => Promise; + searchWithSearXNG: ( + id: string, + data: SearchQuery, + aiSummary?: boolean, + ) => Promise; text2image: (id: string, data: DallEImageItem[]) => Promise; + toggleDallEImageLoading: (key: string, value: boolean) => void; + toggleSearchLoading: (id: string, loading: boolean) => void; updateImageItem: (id: string, updater: (data: DallEImageItem[]) => void) => Promise; useFetchDalleImageItem: (id: string) => SWRResponse; } @@ -83,12 +104,80 @@ export const chatToolSlice: StateCreator< }); }); }, + reSearchWithSearXNG: async (id, data, options) => { + get().toggleSearchLoading(id, true); + await get().updatePluginArguments(id, data); + + await get().searchWithSearXNG(id, data, options?.aiSummary); + }, + saveSearXNGSearchResult: async (id) => { + const message = chatSelectors.getMessageById(id)(get()); + if (!message || !message.plugin) return; + + const { internal_addToolToAssistantMessage, internal_createMessage, openToolUI } = get(); + // 1. 创建一个新的 tool call message + const newToolCallId = `tool_call_${nanoid()}`; + + const toolMessage: CreateMessageParams = { + content: message.content, + id: undefined, + parentId: message.parentId, + plugin: message.plugin, + pluginState: message.pluginState, + role: 'tool', + sessionId: get().activeId, + tool_call_id: newToolCallId, + topicId: get().activeTopicId, + }; + + const addToolItem = async () => { + if (!message.parentId || !message.plugin) return; + + await internal_addToolToAssistantMessage(message.parentId, { + id: newToolCallId, + ...message.plugin, + }); + }; + + const [newMessageId] = await Promise.all([ + // 1. 添加 tool message + internal_createMessage(toolMessage), + // 2. 将这条 tool call message 插入到 ai 消息的 tools 中 + addToolItem(), + ]); + + // 将新创建的 tool message 激活 + openToolUI(newMessageId, message.plugin.identifier); + }, + searchWithSearXNG: async (id, params, aiSummary = true) => { + get().toggleSearchLoading(id, true); + const data = await searchService.search(params.query, params.searchEngines); + await get().updatePluginState(id, data); + + get().toggleSearchLoading(id, false); + + // 只取前 5 个结果作为上下文 + const searchContent: SearchContent[] = data.results.slice(0, 5).map((item) => ({ + content: item.content, + title: item.title, + url: item.url, + })); + + await get().internal_updateMessageContent(id, JSON.stringify(searchContent)); + + // 如果没搜索到结果,那么不触发 ai 总结 + if (searchContent.length === 0) return; + + // 如果 aiSummary 为 true,则会自动触发总结 + return aiSummary; + }, text2image: async (id, data) => { // const isAutoGen = settingsSelectors.isDalleAutoGenerating(useGlobalStore.getState()); // if (!isAutoGen) return; await get().generateImageFromPrompts(data, id); }, + toggleDallEImageLoading: (key, value) => { set( { dalleImageLoading: { ...get().dalleImageLoading, [key]: value } }, @@ -96,6 +185,11 @@ export const chatToolSlice: StateCreator< n('toggleDallEImageLoading'), ); }, + + toggleSearchLoading: (id, loading) => { + set({ searchLoading: { ...get().searchLoading, [id]: loading } }, false, 'toggleSearchLoading'); + }, + updateImageItem: async (id, updater) => { const message = chatSelectors.getMessageById(id)(get()); if (!message) return; @@ -105,6 +199,7 @@ export const chatToolSlice: StateCreator< const nextContent = produce(data, updater); await get().internal_updateMessageContent(id, JSON.stringify(nextContent)); }, + useFetchDalleImageItem: (id) => useClientDataSWR([SWR_FETCH_KEY, id], async () => { const item = await fileService.getFile(id); diff --git a/src/store/chat/slices/builtinTool/initialState.ts b/src/store/chat/slices/builtinTool/initialState.ts index e74bcf6605ac..45f7a6605ac8 100644 --- a/src/store/chat/slices/builtinTool/initialState.ts +++ b/src/store/chat/slices/builtinTool/initialState.ts @@ -3,9 +3,11 @@ import { FileItem } from '@/types/files'; export interface ChatToolState { dalleImageLoading: Record; dalleImageMap: Record; + searchLoading: Record; } export const initialToolState: ChatToolState = { dalleImageLoading: {}, dalleImageMap: {}, + searchLoading: {}, }; diff --git a/src/store/chat/slices/builtinTool/selectors.ts b/src/store/chat/slices/builtinTool/selectors.ts index ce1de8403eec..d87dff2d2a76 100644 --- a/src/store/chat/slices/builtinTool/selectors.ts +++ b/src/store/chat/slices/builtinTool/selectors.ts @@ -5,7 +5,10 @@ const isDallEImageGenerating = (id: string) => (s: ChatStoreState) => s.dalleIma const isGeneratingDallEImage = (s: ChatStoreState) => Object.values(s.dalleImageLoading).some(Boolean); +const isSearXNGSearching = (id: string) => (s: ChatStoreState) => s.searchLoading[id]; + export const chatToolSelectors = { isDallEImageGenerating, isGeneratingDallEImage, + isSearXNGSearching, }; diff --git a/src/tools/index.ts b/src/tools/index.ts index 6dc1aaf90000..1a153842978d 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -2,6 +2,7 @@ import { LobeBuiltinTool } from '@/types/tool'; import { ArtifactsManifest } from './artifacts'; import { DalleManifest } from './dalle'; +import { WebBrowsingManifest } from './web-browsing'; export const builtinTools: LobeBuiltinTool[] = [ { @@ -14,4 +15,9 @@ export const builtinTools: LobeBuiltinTool[] = [ manifest: DalleManifest, type: 'builtin', }, + { + identifier: WebBrowsingManifest.identifier, + manifest: WebBrowsingManifest, + type: 'builtin', + }, ]; diff --git a/src/tools/portals.ts b/src/tools/portals.ts index 89aa42af4fd1..bee827140562 100644 --- a/src/tools/portals.ts +++ b/src/tools/portals.ts @@ -1,3 +1,8 @@ import { BuiltinPortal } from '@/types/tool'; -export const BuiltinToolsPortals: Record = {}; +import { WebBrowsingManifest } from './web-browsing'; +import WebBrowsing from './web-browsing/Portal'; + +export const BuiltinToolsPortals: Record = { + [WebBrowsingManifest.identifier]: WebBrowsing as BuiltinPortal, +}; diff --git a/src/tools/renders.ts b/src/tools/renders.ts index 122a7d412d81..a3cb6ad820f8 100644 --- a/src/tools/renders.ts +++ b/src/tools/renders.ts @@ -2,7 +2,10 @@ import { BuiltinRender } from '@/types/tool'; import { DalleManifest } from './dalle'; import DalleRender from './dalle/Render'; +import { WebBrowsingManifest } from './web-browsing'; +import WebBrowsing from './web-browsing/Render'; export const BuiltinToolsRenders: Record = { [DalleManifest.identifier]: DalleRender as BuiltinRender, + [WebBrowsingManifest.identifier]: WebBrowsing as BuiltinRender, }; diff --git a/src/tools/web-browsing/Portal/ResultList/SearchItem/CategoryAvatar.tsx b/src/tools/web-browsing/Portal/ResultList/SearchItem/CategoryAvatar.tsx new file mode 100644 index 000000000000..57023a8e37b0 --- /dev/null +++ b/src/tools/web-browsing/Portal/ResultList/SearchItem/CategoryAvatar.tsx @@ -0,0 +1,70 @@ +import { Avatar, Icon } from '@lobehub/ui'; +import { useTheme } from 'antd-style'; +import { + LucideAtom, + LucideClapperboard, + LucideFiles, + LucideImages, + LucideLaptop, + LucideMusic, + LucideNewspaper, + LucideShoppingBag, + LucideTextSearch, + LucideUserRound, +} from 'lucide-react'; +import { memo, useMemo } from 'react'; + +interface CategoryAvatarProps { + category: string; + size?: number; +} + +const CategoryAvatar = memo(({ category, size = 24 }) => { + const theme = useTheme(); + + const categoryIcon = useMemo(() => { + switch (category) { + default: + case 'general': { + return LucideTextSearch; + } + case 'videos': { + return LucideClapperboard; + } + case 'images': { + return LucideImages; + } + case 'files': { + return LucideFiles; + } + case 'music': { + return LucideMusic; + } + case 'shopping': { + return LucideShoppingBag; + } + case 'social': { + return LucideUserRound; + } + case 'it': { + return LucideLaptop; + } + case 'news': { + return LucideNewspaper; + } + case 'science': { + return LucideAtom; + } + } + }, [category]); + + return ( + } + background={theme.colorFillTertiary} + size={size} + /> + ); +}); + +export default CategoryAvatar; diff --git a/src/tools/web-browsing/Portal/ResultList/SearchItem/TitleExtra.tsx b/src/tools/web-browsing/Portal/ResultList/SearchItem/TitleExtra.tsx new file mode 100644 index 000000000000..e72a6c228a4b --- /dev/null +++ b/src/tools/web-browsing/Portal/ResultList/SearchItem/TitleExtra.tsx @@ -0,0 +1,38 @@ +import { Tooltip } from '@lobehub/ui'; +import { Tag, Typography } from 'antd'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; + +import CategoryAvatar from './CategoryAvatar'; + +interface TitleExtraProps { + category: string; + highlight?: boolean; + score: number; +} + +const TitleExtra = memo(({ category, score, highlight }) => { + const { t } = useTranslation('tool'); + + return ( + + + {highlight ? ( + + {score.toFixed(1)} + + ) : ( + + {score.toFixed(1)} + + )} + + + + ); +}); +export default TitleExtra; diff --git a/src/tools/web-browsing/Portal/ResultList/SearchItem/Video.tsx b/src/tools/web-browsing/Portal/ResultList/SearchItem/Video.tsx new file mode 100644 index 000000000000..7f3028269538 --- /dev/null +++ b/src/tools/web-browsing/Portal/ResultList/SearchItem/Video.tsx @@ -0,0 +1,136 @@ +import { Avatar as AntAvatar, Typography } from 'antd'; +import { createStyles } from 'antd-style'; +import { memo, useState } from 'react'; +import { Flexbox } from 'react-layout-kit'; + +import { SearchResult } from '@/types/tool/search'; + +import { ENGINE_ICON_MAP } from '../../../const'; +import TitleExtra from './TitleExtra'; + +const useStyles = createStyles(({ css, token }) => { + return { + container: css` + display: flex; + flex: 1; + + padding: 8px; + + color: initial; + + border-radius: 8px; + + &:hover { + background: ${token.colorFillTertiary}; + } + `, + desc: css` + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + + color: ${token.colorTextTertiary}; + text-overflow: ellipsis; + `, + displayLink: css` + color: ${token.colorTextQuaternary}; + `, + iframe: css` + border: 1px solid ${token.colorBorder}; + border-radius: 8px; + `, + title: css` + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + + font-size: 16px; + color: ${token.colorLink}; + text-overflow: ellipsis; + `, + url: css` + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + + color: ${token.colorTextDescription}; + text-overflow: ellipsis; + `, + }; +}); + +interface SearchResultProps extends SearchResult { + highlight?: boolean; +} +const VideoItem = memo( + ({ content, url, iframe_src, highlight, score, engines, title, category }) => { + const { styles, theme } = useStyles(); + + const [expand, setExpand] = useState(false); + return ( + + setExpand(!expand)}> + + {iframe_src && ( + +