UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

215 lines (184 loc) 6.65 kB
import { StateCreator } from 'zustand/vanilla'; import { searchService } from '@/services/search'; import { chatSelectors } from '@/store/chat/selectors'; import { ChatStore } from '@/store/chat/store'; import { CRAWL_CONTENT_LIMITED_COUNT } from '@/tools/web-browsing/const'; import { CreateMessageParams } from '@/types/message'; import { SEARCH_SEARXNG_NOT_CONFIG, SearchContent, SearchQuery, UniformSearchResponse, } from '@/types/tool/search'; import { nanoid } from '@/utils/uuid'; export interface SearchAction { crawlMultiPages: ( id: string, params: { urls: string[] }, aiSummary?: boolean, ) => Promise<boolean | undefined>; crawlSinglePage: ( id: string, params: { url: string }, aiSummary?: boolean, ) => Promise<boolean | undefined>; saveSearchResult: (id: string) => Promise<void>; search: (id: string, data: SearchQuery, aiSummary?: boolean) => Promise<void | boolean>; togglePageContent: (url: string) => void; toggleSearchLoading: (id: string, loading: boolean) => void; /** * 重新发起搜索 * @description 会更新插件的 arguments 参数,然后再次搜索 */ triggerSearchAgain: ( id: string, data: SearchQuery, options?: { aiSummary: boolean }, ) => Promise<void>; } export const searchSlice: StateCreator< ChatStore, [['zustand/devtools', never]], [], SearchAction > = (set, get) => ({ crawlMultiPages: async (id, params, aiSummary = true) => { const { internal_updateMessageContent } = get(); get().toggleSearchLoading(id, true); try { const response = await searchService.crawlPages(params.urls); await get().updatePluginState(id, response); get().toggleSearchLoading(id, false); const { results } = response; if (!results) return; const content = results.map((item) => 'errorMessage' in item ? item : { ...item.data, // if crawl too many content // slice the top 10000 char content: item.data.content?.slice(0, CRAWL_CONTENT_LIMITED_COUNT), }, ); await internal_updateMessageContent(id, JSON.stringify(content)); // if aiSummary is true, then trigger ai message return aiSummary; } catch (e) { const err = e as Error; console.error(e); const content = [{ ...err, errorMessage: err.message, errorType: err.name }]; await internal_updateMessageContent(id, JSON.stringify(content)); } }, crawlSinglePage: async (id, params, aiSummary) => { const { crawlMultiPages } = get(); return await crawlMultiPages(id, { urls: [params.url] }, aiSummary); }, saveSearchResult: 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(), ]); if (!newMessageId) return; // 将新创建的 tool message 激活 openToolUI(newMessageId, message.plugin.identifier); }, search: async (id, { query, ...params }, aiSummary = true) => { get().toggleSearchLoading(id, true); let data: UniformSearchResponse | undefined; try { // 首次查询 data = await searchService.search(query, params); // 如果没有搜索到结果,则执行第一次重试(移除搜索引擎限制) if ( data?.results.length === 0 && params?.searchEngines && params?.searchEngines?.length > 0 ) { const paramsExcludeSearchEngines = { ...params, searchEngines: undefined, }; data = await searchService.search(query, paramsExcludeSearchEngines); get().updatePluginArguments(id, paramsExcludeSearchEngines); } // 如果仍然没有搜索到结果,则执行第二次重试(移除所有限制) if (data?.results.length === 0) { data = await searchService.search(query); get().updatePluginArguments(id, { query }); } await get().updatePluginState(id, data); } catch (e) { if ((e as Error).message === SEARCH_SEARXNG_NOT_CONFIG) { await get().internal_updateMessagePluginError(id, { body: { provider: 'searxng', }, message: 'SearXNG is not configured', type: 'PluginSettingsInvalid', }); } else { await get().internal_updateMessagePluginError(id, { body: e, message: (e as Error).message, type: 'PluginServerError', }); } } get().toggleSearchLoading(id, false); if (!data) return; // add 15 search results to message content const searchContent: SearchContent[] = data.results.slice(0, 15).map((item) => ({ title: item.title, url: item.url, ...(item.content && { content: item.content }), ...(item.publishedDate && { publishedDate: item.publishedDate }), ...(item.imgSrc && { imgSrc: item.imgSrc }), ...(item.thumbnail && { thumbnail: item.thumbnail }), })); await get().internal_updateMessageContent(id, JSON.stringify(searchContent)); // 如果 aiSummary 为 true,则会自动触发总结 return aiSummary; }, togglePageContent: (url) => { set({ activePageContentUrl: url }); }, toggleSearchLoading: (id, loading) => { set( { searchLoading: { ...get().searchLoading, [id]: loading } }, false, `toggleSearchLoading/${loading ? 'start' : 'end'}`, ); }, triggerSearchAgain: async (id, data, options) => { get().toggleSearchLoading(id, true); await get().updatePluginArguments(id, data); await get().search(id, data, options?.aiSummary); }, });