From 97a756dcc2903c53a38e87c91bc9b033709d2089 Mon Sep 17 00:00:00 2001 From: Bob Du Date: Mon, 26 May 2025 22:46:13 +0800 Subject: [PATCH 1/5] feat: add web search switch button Signed-off-by: Bob Du --- service/src/routes/room.ts | 18 ++++++++++++++++ service/src/storage/model.ts | 7 +++---- service/src/storage/mongo.ts | 13 +++++++++++- src/api/index.ts | 7 +++++++ src/locales/en-US.ts | 6 ++++++ src/locales/zh-CN.ts | 6 ++++++ src/store/modules/chat/index.ts | 10 +++++++++ src/typings/chat.d.ts | 1 + src/views/chat/components/Header/index.vue | 12 +++++++++++ src/views/chat/index.vue | 24 ++++++++++++++++++++-- 10 files changed, 97 insertions(+), 7 deletions(-) diff --git a/service/src/routes/room.ts b/service/src/routes/room.ts index e2e9e4b4..f3bd7a5e 100644 --- a/service/src/routes/room.ts +++ b/service/src/routes/room.ts @@ -9,6 +9,7 @@ import { renameChatRoom, updateRoomChatModel, updateRoomPrompt, + updateRoomSearchEnabled, updateRoomUsingContext, } from '../storage/mongo' @@ -27,6 +28,7 @@ router.get('/chatrooms', auth, async (req, res) => { prompt: r.prompt, usingContext: r.usingContext === undefined ? true : r.usingContext, chatModel: r.chatModel, + searchEnabled: !!r.searchEnabled, }) }) res.send({ status: 'Success', message: null, data: result }) @@ -135,6 +137,22 @@ router.post('/room-chatmodel', auth, async (req, res) => { } }) +router.post('/room-search-enabled', auth, async (req, res) => { + try { + const userId = req.headers.userId as string + const { searchEnabled, roomId } = req.body as { searchEnabled: boolean; roomId: number } + const success = await updateRoomSearchEnabled(userId, roomId, searchEnabled) + if (success) + res.send({ status: 'Success', message: 'Saved successfully', data: null }) + else + res.send({ status: 'Fail', message: 'Saved Failed', data: null }) + } + catch (error) { + console.error(error) + res.send({ status: 'Fail', message: 'Update error', data: null }) + } +}) + router.post('/room-context', auth, async (req, res) => { try { const userId = req.headers.userId as string diff --git a/service/src/storage/model.ts b/service/src/storage/model.ts index 794110b1..b7f2eb14 100644 --- a/service/src/storage/model.ts +++ b/service/src/storage/model.ts @@ -81,17 +81,16 @@ export class ChatRoom { prompt: string usingContext: boolean status: Status = Status.Normal - // only access token used - accountId?: string chatModel: string - constructor(userId: string, title: string, roomId: number, chatModel: string) { + searchEnabled: boolean + constructor(userId: string, title: string, roomId: number, chatModel: string, searchEnabled: boolean) { this.userId = userId this.title = title this.prompt = undefined this.roomId = roomId this.usingContext = true - this.accountId = null this.chatModel = chatModel + this.searchEnabled = searchEnabled } } diff --git a/service/src/storage/mongo.ts b/service/src/storage/mongo.ts index 54d1a5d0..95575390 100644 --- a/service/src/storage/mongo.ts +++ b/service/src/storage/mongo.ts @@ -127,7 +127,7 @@ export async function insertChatUsage(userId: ObjectId, } export async function createChatRoom(userId: string, title: string, roomId: number, chatModel: string) { - const room = new ChatRoom(userId, title, roomId, chatModel) + const room = new ChatRoom(userId, title, roomId, chatModel, false) await roomCol.insertOne(room) return room } @@ -182,6 +182,17 @@ export async function updateRoomChatModel(userId: string, roomId: number, chatMo return result.modifiedCount > 0 } +export async function updateRoomSearchEnabled(userId: string, roomId: number, searchEnabled: boolean) { + const query = { userId, roomId } + const update = { + $set: { + searchEnabled, + }, + } + const result = await roomCol.updateOne(query, update) + return result.modifiedCount > 0 +} + export async function getChatRooms(userId: string) { const cursor = roomCol.find({ userId, status: { $ne: Status.Deleted } }) const rooms = [] diff --git a/src/api/index.ts b/src/api/index.ts index 11757e71..84e6ef4a 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -271,6 +271,13 @@ export function fetchUpdateChatRoomUsingContext(using: boolean, roomId: }) } +export function fetchUpdateChatRoomSearchEnabled(searchEnabled: boolean, roomId: number) { + return post({ + url: '/room-search-enabled', + data: { searchEnabled, roomId }, + }) +} + export function fetchDeleteChatRoom(roomId: number) { return post({ url: '/room-delete', diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index d33c1c72..6b0ef3c2 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -55,10 +55,16 @@ export default { usingContext: 'Context Mode', turnOnContext: 'In the current mode, sending messages will carry previous chat records.', turnOffContext: 'In the current mode, sending messages will not carry previous chat records.', + turnOnSearch: 'Web search has been enabled for this chat.', + turnOffSearch: 'Web search has been disabled for this chat.', clickTurnOnContext: 'Click to enable sending messages will carry previous chat records.', clickTurnOffContext: 'Click to disable sending messages will carry previous chat records.', + clickTurnOnSearch: 'Click to enable web search for this chat.', + clickTurnOffSearch: 'Click to disable web search for this chat.', showOnContext: 'Include context', showOffContext: 'Not include context', + searchEnabled: 'Search enabled', + searchDisabled: 'Search disabled', deleteMessage: 'Delete Message', deleteMessageConfirm: 'Are you sure to delete this message?', deleteHistoryConfirm: 'Are you sure to clear this history?', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 6d037b6f..a55693d2 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -55,10 +55,16 @@ export default { usingContext: '上下文模式', turnOnContext: '当前模式下, 发送消息会携带之前的聊天记录', turnOffContext: '当前模式下, 发送消息不会携带之前的聊天记录', + turnOnSearch: '已开启网络搜索功能', + turnOffSearch: '已关闭网络搜索功能', clickTurnOnContext: '点击开启包含上下文', clickTurnOffContext: '点击停止包含上下文', + clickTurnOnSearch: '点击开启网络搜索功能', + clickTurnOffSearch: '点击关闭网络搜索功能', showOnContext: '包含上下文', showOffContext: '不含上下文', + searchEnabled: '搜索已开启', + searchDisabled: '搜索已关闭', deleteMessage: '删除消息', deleteMessageConfirm: '是否删除此消息?', deleteHistoryConfirm: '确定删除此记录?', diff --git a/src/store/modules/chat/index.ts b/src/store/modules/chat/index.ts index 56748970..b0433232 100644 --- a/src/store/modules/chat/index.ts +++ b/src/store/modules/chat/index.ts @@ -11,6 +11,7 @@ import { fetchGetChatRooms, fetchRenameChatRoom, fetchUpdateChatRoomChatModel, + fetchUpdateChatRoomSearchEnabled, fetchUpdateChatRoomUsingContext, fetchUpdateUserChatModel, } from '@/api' @@ -115,6 +116,15 @@ export const useChatStore = defineStore('chat-store', { await fetchUpdateUserChatModel(chatModel) }, + async setChatSearchEnabled(searchEnabled: boolean, roomId: number) { + const index = this.history.findIndex(item => item.uuid === this.active) + if (index !== -1) { + this.history[index].searchEnabled = searchEnabled + await fetchUpdateChatRoomSearchEnabled(searchEnabled, roomId) + this.recordState() + } + }, + async addHistory(history: Chat.History, chatData: Chat.Chat[] = []) { await fetchCreateChatRoom(history.title, history.uuid, history.chatModel) this.history.unshift(history) diff --git a/src/typings/chat.d.ts b/src/typings/chat.d.ts index b319e3fc..7d879228 100644 --- a/src/typings/chat.d.ts +++ b/src/typings/chat.d.ts @@ -29,6 +29,7 @@ declare namespace Chat { prompt?: string usingContext: boolean chatModel?: string + searchEnabled?: boolean } interface ChatState { diff --git a/src/views/chat/components/Header/index.vue b/src/views/chat/components/Header/index.vue index f0efabb3..bec05e68 100644 --- a/src/views/chat/components/Header/index.vue +++ b/src/views/chat/components/Header/index.vue @@ -7,12 +7,14 @@ import IconPrompt from '@/icons/Prompt.vue' interface Props { usingContext: boolean showPrompt: boolean + searchEnabled?: boolean } interface Emit { (ev: 'export'): void (ev: 'toggleUsingContext'): void (ev: 'toggleShowPrompt'): void + (ev: 'toggleSearchEnabled'): void } defineProps() @@ -43,6 +45,10 @@ function toggleUsingContext() { emit('toggleUsingContext') } +function toggleSearchEnabled() { + emit('toggleSearchEnabled') +} + function handleShowPrompt() { emit('toggleShowPrompt') } @@ -80,6 +86,12 @@ function handleShowPrompt() { {{ usingContext ? $t('chat.showOnContext') : $t('chat.showOffContext') }} + + + + + {{ searchEnabled ? $t('chat.searchEnabled') : $t('chat.searchDisabled') }} + diff --git a/src/views/chat/index.vue b/src/views/chat/index.vue index 7fc1d35f..012cc073 100644 --- a/src/views/chat/index.vue +++ b/src/views/chat/index.vue @@ -552,6 +552,19 @@ async function handleScroll(event: any) { prevScrollTop = scrollTop } +async function handleToggleSearchEnabled() { + if (!currentChatHistory.value) + return + + const searchEnabled = currentChatHistory.value.searchEnabled ?? false + currentChatHistory.value.searchEnabled = !searchEnabled + await chatStore.setChatSearchEnabled(!searchEnabled, +uuid) + if (currentChatHistory.value.searchEnabled) + ms.success(t('chat.turnOnSearch')) + else + ms.warning(t('chat.turnOffSearch')) +} + async function handleToggleUsingContext() { if (!currentChatHistory.value) return @@ -662,7 +675,10 @@ onUnmounted(() => { v-if="isMobile" :using-context="usingContext" :show-prompt="showPrompt" - @export="handleExport" @toggle-using-context="handleToggleUsingContext" + :search-enabled="currentChatHistory?.searchEnabled" + @export="handleExport" + @toggle-using-context="handleToggleUsingContext" + @toggle-search-enabled="handleToggleSearchEnabled" @toggle-show-prompt="showPrompt = true" />
@@ -767,7 +783,6 @@ onUnmounted(() => { - { :disabled="!!authStore.session?.auth && !authStore.token && !authStore.session?.authProxyEnabled" @update-value="(val) => handleSyncChatModel(val)" /> + + + + +
From 3e324120ed70b6b774f2885fbe431f58f4f84e81 Mon Sep 17 00:00:00 2001 From: Bob Du Date: Tue, 27 May 2025 10:26:20 +0800 Subject: [PATCH 2/5] refactor: extracting create content helper function Signed-off-by: Bob Du --- service/src/chatgpt/index.ts | 70 ++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/service/src/chatgpt/index.ts b/service/src/chatgpt/index.ts index e8d8a7b7..0aa915f5 100644 --- a/service/src/chatgpt/index.ts +++ b/service/src/chatgpt/index.ts @@ -85,25 +85,7 @@ async function chatReplyProcess(options: RequestOptions) { } // Prepare the user message content (text and images) - let content: string | OpenAI.Chat.ChatCompletionContentPart[] = message - - // Handle image uploads if present - if (uploadFileKeys && uploadFileKeys.length > 0) { - content = [ - { - type: 'text', - text: message, - }, - ] - for (const uploadFileKey of uploadFileKeys) { - content.push({ - type: 'image_url', - image_url: { - url: await convertImageUrl(uploadFileKey), - }, - }) - } - } + const content: string | OpenAI.Chat.ChatCompletionContentPart[] = await createContent(message, uploadFileKeys) // Add the user message messages.push({ @@ -244,26 +226,7 @@ async function getMessageById(id: string): Promise { } else { if (isPrompt) { // prompt - let content: string | OpenAI.Chat.ChatCompletionContentPart[] = chatInfo.prompt - if (chatInfo.images && chatInfo.images.length > 0) { - content = [ - { - type: 'text', - text: chatInfo.prompt, - }, - ] - for (const image of chatInfo.images) { - const imageUrlBase64 = await convertImageUrl(image) - if (imageUrlBase64) { - content.push({ - type: 'image_url', - image_url: { - url: await convertImageUrl(image), - }, - }) - } - } - } + const content: string | OpenAI.Chat.ChatCompletionContentPart[] = await createContent(chatInfo.prompt, chatInfo.images) return { id, parentMessageId, @@ -311,6 +274,35 @@ async function getRandomApiKey(user: UserInfo, chatModel: string): Promise { + // If no images or empty array, return just the text + if (!images || images.length === 0) + return text + + // Create content with text and images + const content: OpenAI.Chat.ChatCompletionContentPart[] = [ + { + type: 'text', + text, + }, + ] + + for (const image of images) { + const imageUrl = await convertImageUrl(image) + if (imageUrl) { + content.push({ + type: 'image_url', + image_url: { + url: imageUrl, + }, + }) + } + } + + return content +} + // Helper function to add previous messages to the conversation context async function addPreviousMessages(parentMessageId: string, maxContextCount: number, messages: OpenAI.Chat.ChatCompletionMessageParam[]): Promise { // Recursively get previous messages From 925c8c3bb7368b5744cbcba0e2a162be70ac4b68 Mon Sep 17 00:00:00 2001 From: Bob Du Date: Tue, 27 May 2025 17:23:49 +0800 Subject: [PATCH 3/5] feat: add search api provider and api key config page Signed-off-by: Bob Du --- service/src/index.ts | 27 +++++ service/src/storage/config.ts | 7 +- service/src/storage/model.ts | 15 +++ src/api/index.ts | 16 ++- src/components/common/Setting/Search.vue | 121 +++++++++++++++++++++++ src/components/common/Setting/index.vue | 8 ++ src/components/common/Setting/model.ts | 13 +++ src/locales/en-US.ts | 5 + src/locales/zh-CN.ts | 5 + 9 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 src/components/common/Setting/Search.vue diff --git a/service/src/index.ts b/service/src/index.ts index 72bebc72..e8bdd09e 100644 --- a/service/src/index.ts +++ b/service/src/index.ts @@ -781,6 +781,33 @@ router.post('/audit-test', rootAuth, async (req, res) => { } }) +router.post('/setting-search', rootAuth, async (req, res) => { + try { + const config = req.body as import('./storage/model').SearchConfig + + const thisConfig = await getOriginConfig() + thisConfig.searchConfig = config + const result = await updateConfig(thisConfig) + clearConfigCache() + res.send({ status: 'Success', message: '操作成功 | Successfully', data: result.searchConfig }) + } + catch (error) { + res.send({ status: 'Fail', message: error.message, data: null }) + } +}) + +router.post('/search-test', rootAuth, async (req, res) => { + try { + const { search, text } = req.body as { search: import('./storage/model').SearchConfig; text: string } + // TODO: Implement actual search test logic with Tavily API + // For now, just return a success response + res.send({ status: 'Success', message: '搜索测试成功 | Search test successful', data: { query: text, results: [] } }) + } + catch (error) { + res.send({ status: 'Fail', message: error.message, data: null }) + } +}) + router.post('/setting-advanced', auth, async (req, res) => { try { const config = req.body as { diff --git a/service/src/storage/config.ts b/service/src/storage/config.ts index 38cf71a4..c95ffabb 100644 --- a/service/src/storage/config.ts +++ b/service/src/storage/config.ts @@ -2,7 +2,7 @@ import { ObjectId } from 'mongodb' import * as dotenv from 'dotenv' import type { TextAuditServiceProvider } from 'src/utils/textAudit' import { isNotEmptyString, isTextAuditServiceProvider } from '../utils/is' -import { AdvancedConfig, AnnounceConfig, AuditConfig, Config, KeyConfig, MailConfig, SiteConfig, TextAudioType, UserRole } from './model' +import { AdvancedConfig, AnnounceConfig, AuditConfig, Config, KeyConfig, MailConfig, SearchConfig, SiteConfig, TextAudioType, UserRole } from './model' import { getConfig, getKeys, upsertKey } from './mongo' dotenv.config() @@ -110,6 +110,11 @@ export async function getOriginConfig() { ) } + if (!config.searchConfig) { + config.searchConfig = new SearchConfig() + config.searchConfig.enabled = false + } + if (!isNotEmptyString(config.siteConfig.chatModels)) config.siteConfig.chatModels = 'gpt-4.1,gpt-4.1-mini,gpt-4.1-nano' return config diff --git a/service/src/storage/model.ts b/service/src/storage/model.ts index b7f2eb14..fc5eb58a 100644 --- a/service/src/storage/model.ts +++ b/service/src/storage/model.ts @@ -171,6 +171,20 @@ export class ChatUsage { } } +export class SearchConfig { + public enabled: boolean + public provider?: SearchServiceProvider + public options?: SearchServiceOptions +} + +export enum SearchServiceProvider { + Tavily = 'tavily', +} + +export class SearchServiceOptions { + public apiKey: string +} + export class Config { constructor( public _id: ObjectId, @@ -187,6 +201,7 @@ export class Config { public siteConfig?: SiteConfig, public mailConfig?: MailConfig, public auditConfig?: AuditConfig, + public searchConfig?: SearchConfig, public advancedConfig?: AdvancedConfig, public announceConfig?: AnnounceConfig, ) { } diff --git a/src/api/index.ts b/src/api/index.ts index 84e6ef4a..30a69abf 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,6 +1,6 @@ import type { AxiosProgressEvent, GenericAbortSignal } from 'axios' import { get, post } from '@/utils/request' -import type { AnnounceConfig, AuditConfig, ConfigState, GiftCard, KeyConfig, MailConfig, SiteConfig, Status, UserInfo, UserPassword, UserPrompt } from '@/components/common/Setting/model' +import type { AnnounceConfig, AuditConfig, ConfigState, GiftCard, KeyConfig, MailConfig, SearchConfig, SiteConfig, Status, UserInfo, UserPassword, UserPrompt } from '@/components/common/Setting/model' import { useAuthStore, useUserStore } from '@/store' import type { SettingsState } from '@/store/modules/user/helper' @@ -340,6 +340,20 @@ export function fetchTestAudit(text: string, audit: AuditConfig) { }) } +export function fetchUpdateSearch(search: SearchConfig) { + return post({ + url: '/setting-search', + data: search, + }) +} + +export function fetchTestSearch(text: string, search: SearchConfig) { + return post({ + url: '/search-test', + data: { search, text }, + }) +} + export function fetchUpdateAnnounce(announce: AnnounceConfig) { return post({ url: '/setting-announce', diff --git a/src/components/common/Setting/Search.vue b/src/components/common/Setting/Search.vue new file mode 100644 index 00000000..692fe88c --- /dev/null +++ b/src/components/common/Setting/Search.vue @@ -0,0 +1,121 @@ + + + diff --git a/src/components/common/Setting/index.vue b/src/components/common/Setting/index.vue index b671f6e1..c7a9d554 100644 --- a/src/components/common/Setting/index.vue +++ b/src/components/common/Setting/index.vue @@ -8,6 +8,7 @@ import About from './About.vue' import Site from './Site.vue' import Mail from './Mail.vue' import Audit from './Audit.vue' +import Search from './Search.vue' import Gift from './Gift.vue' import User from './User.vue' import Key from './Keys.vue' @@ -143,6 +144,13 @@ const show = computed({ + + + +