Skip to content

Commit 925c8c3

Browse files
committed
feat: add search api provider and api key config page
Signed-off-by: Bob Du <[email protected]>
1 parent 3e32412 commit 925c8c3

File tree

9 files changed

+215
-2
lines changed

9 files changed

+215
-2
lines changed

service/src/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,33 @@ router.post('/audit-test', rootAuth, async (req, res) => {
781781
}
782782
})
783783

784+
router.post('/setting-search', rootAuth, async (req, res) => {
785+
try {
786+
const config = req.body as import('./storage/model').SearchConfig
787+
788+
const thisConfig = await getOriginConfig()
789+
thisConfig.searchConfig = config
790+
const result = await updateConfig(thisConfig)
791+
clearConfigCache()
792+
res.send({ status: 'Success', message: '操作成功 | Successfully', data: result.searchConfig })
793+
}
794+
catch (error) {
795+
res.send({ status: 'Fail', message: error.message, data: null })
796+
}
797+
})
798+
799+
router.post('/search-test', rootAuth, async (req, res) => {
800+
try {
801+
const { search, text } = req.body as { search: import('./storage/model').SearchConfig; text: string }
802+
// TODO: Implement actual search test logic with Tavily API
803+
// For now, just return a success response
804+
res.send({ status: 'Success', message: '搜索测试成功 | Search test successful', data: { query: text, results: [] } })
805+
}
806+
catch (error) {
807+
res.send({ status: 'Fail', message: error.message, data: null })
808+
}
809+
})
810+
784811
router.post('/setting-advanced', auth, async (req, res) => {
785812
try {
786813
const config = req.body as {

service/src/storage/config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ObjectId } from 'mongodb'
22
import * as dotenv from 'dotenv'
33
import type { TextAuditServiceProvider } from 'src/utils/textAudit'
44
import { isNotEmptyString, isTextAuditServiceProvider } from '../utils/is'
5-
import { AdvancedConfig, AnnounceConfig, AuditConfig, Config, KeyConfig, MailConfig, SiteConfig, TextAudioType, UserRole } from './model'
5+
import { AdvancedConfig, AnnounceConfig, AuditConfig, Config, KeyConfig, MailConfig, SearchConfig, SiteConfig, TextAudioType, UserRole } from './model'
66
import { getConfig, getKeys, upsertKey } from './mongo'
77

88
dotenv.config()
@@ -110,6 +110,11 @@ export async function getOriginConfig() {
110110
)
111111
}
112112

113+
if (!config.searchConfig) {
114+
config.searchConfig = new SearchConfig()
115+
config.searchConfig.enabled = false
116+
}
117+
113118
if (!isNotEmptyString(config.siteConfig.chatModels))
114119
config.siteConfig.chatModels = 'gpt-4.1,gpt-4.1-mini,gpt-4.1-nano'
115120
return config

service/src/storage/model.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,20 @@ export class ChatUsage {
171171
}
172172
}
173173

174+
export class SearchConfig {
175+
public enabled: boolean
176+
public provider?: SearchServiceProvider
177+
public options?: SearchServiceOptions
178+
}
179+
180+
export enum SearchServiceProvider {
181+
Tavily = 'tavily',
182+
}
183+
184+
export class SearchServiceOptions {
185+
public apiKey: string
186+
}
187+
174188
export class Config {
175189
constructor(
176190
public _id: ObjectId,
@@ -187,6 +201,7 @@ export class Config {
187201
public siteConfig?: SiteConfig,
188202
public mailConfig?: MailConfig,
189203
public auditConfig?: AuditConfig,
204+
public searchConfig?: SearchConfig,
190205
public advancedConfig?: AdvancedConfig,
191206
public announceConfig?: AnnounceConfig,
192207
) { }

src/api/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
22
import { get, post } from '@/utils/request'
3-
import type { AnnounceConfig, AuditConfig, ConfigState, GiftCard, KeyConfig, MailConfig, SiteConfig, Status, UserInfo, UserPassword, UserPrompt } from '@/components/common/Setting/model'
3+
import type { AnnounceConfig, AuditConfig, ConfigState, GiftCard, KeyConfig, MailConfig, SearchConfig, SiteConfig, Status, UserInfo, UserPassword, UserPrompt } from '@/components/common/Setting/model'
44
import { useAuthStore, useUserStore } from '@/store'
55
import type { SettingsState } from '@/store/modules/user/helper'
66

@@ -340,6 +340,20 @@ export function fetchTestAudit<T = any>(text: string, audit: AuditConfig) {
340340
})
341341
}
342342

343+
export function fetchUpdateSearch<T = any>(search: SearchConfig) {
344+
return post<T>({
345+
url: '/setting-search',
346+
data: search,
347+
})
348+
}
349+
350+
export function fetchTestSearch<T = any>(text: string, search: SearchConfig) {
351+
return post<T>({
352+
url: '/search-test',
353+
data: { search, text },
354+
})
355+
}
356+
343357
export function fetchUpdateAnnounce<T = any>(announce: AnnounceConfig) {
344358
return post<T>({
345359
url: '/setting-announce',
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<script setup lang='ts'>
2+
import { onMounted, ref } from 'vue'
3+
import { NButton, NInput, NSelect, NSpin, NSwitch, useMessage } from 'naive-ui'
4+
import type { ConfigState, SearchConfig, SearchServiceProvider } from './model'
5+
import { fetchChatConfig, fetchTestSearch, fetchUpdateSearch } from '@/api'
6+
import { t } from '@/locales'
7+
8+
const ms = useMessage()
9+
10+
const loading = ref(false)
11+
const saving = ref(false)
12+
const testing = ref(false)
13+
const testText = ref<string>('What is the latest news about artificial intelligence?')
14+
15+
const serviceOptions: { label: string; key: SearchServiceProvider; value: SearchServiceProvider }[] = [
16+
{ label: 'Tavily', key: 'tavily', value: 'tavily' },
17+
]
18+
19+
const config = ref<SearchConfig>()
20+
21+
async function fetchConfig() {
22+
try {
23+
loading.value = true
24+
const { data } = await fetchChatConfig<ConfigState>()
25+
config.value = data.searchConfig
26+
}
27+
finally {
28+
loading.value = false
29+
}
30+
}
31+
32+
async function updateSearchInfo() {
33+
saving.value = true
34+
try {
35+
const { data } = await fetchUpdateSearch(config.value as SearchConfig)
36+
config.value = data
37+
ms.success(t('common.success'))
38+
}
39+
catch (error: any) {
40+
ms.error(error.message)
41+
}
42+
saving.value = false
43+
}
44+
45+
async function testSearch() {
46+
testing.value = true
47+
try {
48+
const { message } = await fetchTestSearch(testText.value as string, config.value as SearchConfig) as { status: string; message: string }
49+
ms.success(message)
50+
}
51+
catch (error: any) {
52+
ms.error(error.message)
53+
}
54+
testing.value = false
55+
}
56+
57+
onMounted(() => {
58+
fetchConfig()
59+
})
60+
</script>
61+
62+
<template>
63+
<NSpin :show="loading">
64+
<div class="p-4 space-y-5 min-h-[200px]">
65+
<div class="space-y-6">
66+
<div class="flex items-center space-x-4">
67+
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.searchEnabled') }}</span>
68+
<div class="flex-1">
69+
<NSwitch
70+
:round="false" :value="config && config.enabled"
71+
@update:value="(val) => { if (config) config.enabled = val }"
72+
/>
73+
</div>
74+
</div>
75+
<div v-if="config && config.enabled" class="flex items-center space-x-4">
76+
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.searchProvider') }}</span>
77+
<div class="flex-1">
78+
<NSelect
79+
style="width: 140px"
80+
:value="config && config.provider"
81+
:options="serviceOptions"
82+
@update-value="(val) => { if (config) config.provider = val as SearchServiceProvider }"
83+
/>
84+
</div>
85+
</div>
86+
<div v-if="config && config.enabled" class="flex items-center space-x-4">
87+
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.searchApiKey') }}</span>
88+
<div class="flex-1">
89+
<NInput
90+
:value="config && config.options && config.options.apiKey"
91+
placeholder=""
92+
type="password"
93+
show-password-on="click"
94+
@input="(val) => { if (config && config.options) config.options.apiKey = val }"
95+
/>
96+
</div>
97+
</div>
98+
<div v-if="config && config.enabled" class="flex items-center space-x-4">
99+
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.searchTest') }}</span>
100+
<div class="flex-1">
101+
<NInput
102+
v-model:value="testText"
103+
placeholder=""
104+
/>
105+
</div>
106+
</div>
107+
<div class="flex items-center space-x-4">
108+
<span class="flex-shrink-0 w-[100px]" />
109+
<div class="flex flex-wrap items-center gap-4">
110+
<NButton :loading="saving" type="primary" @click="updateSearchInfo()">
111+
{{ $t('common.save') }}
112+
</NButton>
113+
<NButton :loading="testing" type="info" @click="testSearch()">
114+
{{ $t('common.test') }}
115+
</NButton>
116+
</div>
117+
</div>
118+
</div>
119+
</div>
120+
</NSpin>
121+
</template>

src/components/common/Setting/index.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import About from './About.vue'
88
import Site from './Site.vue'
99
import Mail from './Mail.vue'
1010
import Audit from './Audit.vue'
11+
import Search from './Search.vue'
1112
import Gift from './Gift.vue'
1213
import User from './User.vue'
1314
import Key from './Keys.vue'
@@ -143,6 +144,13 @@ const show = computed({
143144
</template>
144145
<Audit />
145146
</NTabPane>
147+
<NTabPane v-if="userStore.userInfo.root" name="SearchConfig" tab="SearchConfig">
148+
<template #tab>
149+
<SvgIcon class="text-lg" icon="mdi:web" />
150+
<span class="ml-2">{{ $t('setting.searchConfig') }}</span>
151+
</template>
152+
<Search />
153+
</NTabPane>
146154
<NTabPane v-if="userStore.userInfo.root" name="UserConfig" tab="UserConfig">
147155
<template #tab>
148156
<SvgIcon class="text-lg" icon="ri-user-5-line" />

src/components/common/Setting/model.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export class ConfigState {
1313
siteConfig?: SiteConfig
1414
mailConfig?: MailConfig
1515
auditConfig?: AuditConfig
16+
searchConfig?: SearchConfig
1617
announceConfig?: AnnounceConfig
1718
}
1819

@@ -180,3 +181,15 @@ export interface GiftCard {
180181
amount: number
181182
redeemed: number
182183
}
184+
185+
export type SearchServiceProvider = 'tavily'
186+
187+
export interface SearchServiceOptions {
188+
apiKey: string
189+
}
190+
191+
export class SearchConfig {
192+
enabled?: boolean
193+
provider?: SearchServiceProvider
194+
options?: SearchServiceOptions
195+
}

src/locales/en-US.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export default {
110110
siteConfig: 'Site Config',
111111
mailConfig: 'Mail Config',
112112
auditConfig: 'Audit Config',
113+
searchConfig: 'Search Config',
113114
avatarLink: 'Avatar Link',
114115
name: 'Name',
115116
description: 'Description',
@@ -163,6 +164,10 @@ export default {
163164
auditBaiduLabelLink: 'Goto Label Detail',
164165
auditCustomizeEnabled: 'Customize',
165166
auditCustomizeWords: 'Sensitive Words',
167+
searchEnabled: 'Search Enabled',
168+
searchProvider: 'Search Provider',
169+
searchApiKey: 'Search API Key',
170+
searchTest: 'Test Search',
166171
accessTokenExpiredTime: 'Expired Time',
167172
userConfig: 'Users',
168173
keysConfig: 'Keys Manager',

src/locales/zh-CN.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export default {
110110
siteConfig: '网站配置',
111111
mailConfig: '邮箱配置',
112112
auditConfig: '敏感词审核',
113+
searchConfig: '搜索配置',
113114
avatarLink: '头像链接',
114115
name: '名称',
115116
description: '描述',
@@ -163,6 +164,10 @@ export default {
163164
auditBaiduLabelLink: '查看细分类型',
164165
auditCustomizeEnabled: '自定义',
165166
auditCustomizeWords: '敏感词',
167+
searchEnabled: '搜索功能',
168+
searchProvider: '搜索提供商',
169+
searchApiKey: '搜索 API 密钥',
170+
searchTest: '测试搜索',
166171
accessTokenExpiredTime: '过期时间',
167172
userConfig: '用户管理',
168173
keysConfig: 'Keys 管理',

0 commit comments

Comments
 (0)