Skip to content

Commit 4d47472

Browse files
authored
feat: add external chat sites feature (#673)
- add external chat sites configuration in site settings - support adding external chat sites with name and URL - display external chat sites in model selector with external link icon - click external chat site to open in new tab - store external chat sites as BSON array in database - add i18n support for external chat sites - separate externalChatSites field from chatModels in API response Signed-off-by: Bob Du <i@bobdu.cc>
1 parent 2680cde commit 4d47472

10 files changed

Lines changed: 142 additions & 3 deletions

File tree

service/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@ router.post('/session', async (req, res) => {
165165
}
166166
})
167167

168+
// Parse external chat sites list
169+
const externalChatSites: Array<{ name: string, url: string }> = config.siteConfig.externalChatSites || []
170+
168171
let userInfo: { name: string, description: string, avatar: string, userId: string, root: boolean, roles: UserRole[], config: UserConfig, advanced: AdvancedConfig }
169172
if (userId != null) {
170173
const user = await getUserById(userId)
@@ -177,8 +180,9 @@ router.post('/session', async (req, res) => {
177180
auth: hasAuth,
178181
allowRegister,
179182
title: config.siteConfig.siteTitle,
180-
chatModels,
183+
chatModels: chatModelOptions,
181184
allChatModels: chatModelOptions,
185+
externalChatSites,
182186
showWatermark: config.siteConfig?.showWatermark,
183187
adminViewChatHistoryEnabled: process.env.ADMIN_VIEW_CHAT_HISTORY_ENABLED === 'true',
184188
},
@@ -333,6 +337,7 @@ router.post('/session', async (req, res) => {
333337
title: config.siteConfig.siteTitle,
334338
chatModels,
335339
allChatModels: chatModelOptions,
340+
externalChatSites,
336341
usageCountLimit: config.siteConfig?.usageCountLimit,
337342
showWatermark: config.siteConfig?.showWatermark,
338343
adminViewChatHistoryEnabled: process.env.ADMIN_VIEW_CHAT_HISTORY_ENABLED === 'true',
@@ -352,6 +357,7 @@ router.post('/session', async (req, res) => {
352357
title: config.siteConfig.siteTitle,
353358
chatModels: chatModelOptions,
354359
allChatModels: chatModelOptions,
360+
externalChatSites,
355361
showWatermark: config.siteConfig?.showWatermark,
356362
adminViewChatHistoryEnabled: process.env.ADMIN_VIEW_CHAT_HISTORY_ENABLED === 'true',
357363
userInfo,

service/src/storage/model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ export class SiteConfig {
256256
public s3Endpoint?: string,
257257
public s3PathPrefix?: string,
258258
public s3CustomDomain?: string,
259+
public externalChatSites?: Array<{ name: string, url: string }>,
259260
) { }
260261
}
261262

src/components/common/Setting/Site.vue

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,67 @@ const saving = ref(false)
1212
1313
const config = ref(new SiteConfig())
1414
15+
interface ExternalChatSite {
16+
name: string
17+
url: string
18+
}
19+
20+
const externalChatSite = ref<ExternalChatSite[]>([])
21+
1522
async function fetchConfig() {
1623
try {
1724
loading.value = true
1825
const { data } = await fetchChatConfig<ConfigState>()
1926
config.value = data.siteConfig ? data.siteConfig : new SiteConfig()
27+
28+
// Parse external chat sites list
29+
if (config.value.externalChatSites && Array.isArray(config.value.externalChatSites)) {
30+
externalChatSite.value = config.value.externalChatSites.map((item: any) => ({
31+
name: item.name || '',
32+
url: item.url || '',
33+
}))
34+
}
35+
else {
36+
externalChatSite.value = []
37+
}
2038
}
2139
finally {
2240
loading.value = false
2341
}
2442
}
2543
44+
function addExternalChatSite() {
45+
externalChatSite.value.push({ name: '', url: '' })
46+
}
47+
48+
function removeExternalChatSite(index: number) {
49+
externalChatSite.value.splice(index, 1)
50+
}
51+
2652
async function updateSiteInfo(site?: SiteConfig) {
2753
if (!site)
2854
return
2955
56+
// Set external chat sites list
57+
const validSites = externalChatSite.value.filter(m => m.name && m.url)
58+
site.externalChatSites = validSites.length > 0 ? validSites : undefined
59+
3060
saving.value = true
3161
try {
3262
const { data } = await fetchUpdateSite(site)
3363
config.value = data
64+
65+
// Update external chat sites list display
66+
if (data.externalChatSites && Array.isArray(data.externalChatSites)) {
67+
externalChatSite.value = data.externalChatSites.map((item: any) => ({
68+
name: item.name || '',
69+
url: item.url || '',
70+
}))
71+
}
72+
else {
73+
externalChatSite.value = []
74+
}
75+
3476
ms.success(t('common.success'))
3577
}
3678
catch (error: any) {
@@ -127,6 +169,31 @@ onMounted(() => {
127169
/>
128170
</div>
129171
</div>
172+
<div class="space-y-4">
173+
<div class="flex items-center space-x-4">
174+
<span class="shrink-0 w-[100px]">{{ t('setting.externalChatSites') }}</span>
175+
<div class="flex-1">
176+
<NButton size="small" @click="addExternalChatSite">
177+
{{ t('common.add') }}
178+
</NButton>
179+
</div>
180+
</div>
181+
<div v-for="(model, index) in externalChatSite" :key="index" class="flex items-center space-x-2 pl-[100px]">
182+
<NInput
183+
v-model:value="model.name"
184+
:placeholder="t('setting.externalModelName')"
185+
style="flex: 1;"
186+
/>
187+
<NInput
188+
v-model:value="model.url"
189+
placeholder="URL"
190+
style="flex: 2;"
191+
/>
192+
<NButton size="small" type="error" @click="removeExternalChatSite(index)">
193+
{{ t('common.delete') }}
194+
</NButton>
195+
</div>
196+
</div>
130197
<!-- 增加新注册用户的全局数量设置 -->
131198
<div class="flex items-center space-x-4">
132199
<span class="shrink-0 w-[100px]">{{ t('setting.globalAmount') }}</span>

src/components/common/Setting/model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export class SiteConfig {
4141
s3Endpoint?: string
4242
s3PathPrefix?: string
4343
s3CustomDomain?: string
44+
externalChatSites?: Array<{ name: string, url: string }>
4445
}
4546

4647
export class MailConfig {

src/locales/en-US.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@
173173
"smtpFrom": "From",
174174
"siteTitle": "Title",
175175
"siteDomain": "Domain",
176+
"externalChatSites": "External Chat Sites",
177+
"externalModelName": "Site Name",
176178
"registerEnabled": "Register Enabled",
177179
"registerReview": "Register Review",
178180
"registerMails": "Register Mails",

src/locales/ko-KR.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@
173173
"smtpFrom": "발신자 이메일",
174174
"siteTitle": "제목",
175175
"siteDomain": "도메인",
176+
"externalChatSites": "외부 채팅 사이트",
177+
"externalModelName": "사이트 이름",
176178
"registerEnabled": "등록 활성화",
177179
"registerReview": "등록 리뷰",
178180
"registerMails": "메일 등록",

src/locales/zh-CN.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@
177177
"smtpFrom": "发件人邮箱",
178178
"siteTitle": "网站标题",
179179
"siteDomain": "域名 不含/",
180+
"externalChatSites": "外部会话站点",
181+
"externalModelName": "站点名称",
180182
"registerEnabled": "新用户",
181183
"registerReview": "新用户审核",
182184
"registerMails": "邮箱后缀",

src/locales/zh-TW.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@
173173
"smtpFrom": "发件人邮箱",
174174
"siteTitle": "网站标题",
175175
"siteDomain": "域名 不含/",
176+
"externalChatSites": "外部會話站點",
177+
"externalModelName": "站點名稱",
176178
"registerEnabled": "新用户",
177179
"registerReview": "新用户审核",
178180
"registerMails": "后缀",

src/store/modules/auth/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ interface SessionResponse {
2222
key: string
2323
value: string
2424
}[]
25+
externalChatSites?: Array<{ name: string, url: string }>
2526
usageCountLimit: boolean
2627
showWatermark: boolean
2728
adminViewChatHistoryEnabled?: boolean

src/views/chat/index.vue

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang='ts'>
22
import type { MessageReactive, UploadFileInfo } from 'naive-ui'
33
import html2canvas from 'html2canvas'
4+
import { h } from 'vue'
45
import {
56
fetchChatAPIProcessSSE,
67
fetchChatResponseoHistory,
@@ -983,8 +984,61 @@ const footerClass = computed(() => {
983984
return classes
984985
})
985986
987+
// Check if it's an external chat site (format: external:name:url)
988+
function isExternalModel(value: string): boolean {
989+
return value.startsWith('external:')
990+
}
991+
992+
// Extract URL from external chat site value
993+
function getExternalModelUrl(value: string): string | null {
994+
if (!isExternalModel(value))
995+
return null
996+
const parts = value.split(':')
997+
if (parts.length >= 3)
998+
return parts.slice(2).join(':') // Handle URLs that may contain : symbol
999+
return null
1000+
}
1001+
1002+
const chatModelOptions = computed(() => {
1003+
const baseModels = authStore.session?.chatModels ?? []
1004+
const externalSites = authStore.session?.externalChatSites ?? []
1005+
1006+
// Convert external chat sites to model options format
1007+
const externalOptions = externalSites.map(site => ({
1008+
label: site.name,
1009+
key: `external:${site.name}`,
1010+
value: `external:${site.name}:${site.url}`,
1011+
}))
1012+
1013+
return [...baseModels, ...externalOptions]
1014+
})
1015+
1016+
function renderChatModelLabel(option: { label: string, value: string }, _selected: boolean) {
1017+
if (isExternalModel(option.value)) {
1018+
return h('span', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, [
1019+
option.label,
1020+
h(SvgIcon, {
1021+
icon: 'ri:external-link-line',
1022+
style: { fontSize: '14px', color: 'var(--n-text-color-secondary)' },
1023+
}),
1024+
])
1025+
}
1026+
return option.label
1027+
}
1028+
9861029
async function handleSyncChatModel(chatModel: string) {
987-
// 保存切换前的模型和 toolsEnabled 状态
1030+
// Check if it's an external chat site, open in new tab if so
1031+
if (isExternalModel(chatModel)) {
1032+
const url = getExternalModelUrl(chatModel)
1033+
if (url) {
1034+
const w = window.open(url, '_blank', 'noopener,noreferrer')
1035+
if (w)
1036+
w.opener = null
1037+
}
1038+
return
1039+
}
1040+
1041+
// Save previous model and toolsEnabled state before switching
9881042
const previousModel = currentChatRoom.value?.chatModel
9891043
const previousToolsEnabled = currentChatRoom.value?.toolsEnabled ?? false
9901044
@@ -1334,8 +1388,9 @@ onUnmounted(() => {
13341388
<NSelect
13351389
style="width: 250px"
13361390
:value="currentChatRoom?.chatModel"
1337-
:options="authStore.session?.chatModels"
1391+
:options="chatModelOptions"
13381392
:disabled="!!authStore.session?.auth && !authStore.token && !authStore.session?.authProxyEnabled"
1393+
:render-label="renderChatModelLabel"
13391394
@update:value="handleSyncChatModel"
13401395
/>
13411396
<HoverButton

0 commit comments

Comments
 (0)