Merge pull request #1086 from Windfarer/storage-allow-list

feat: add STORAGE_ALLOW_LIST env var
This commit is contained in:
lyingbug
2026-04-29 19:49:46 +08:00
committed by GitHub
9 changed files with 284 additions and 246 deletions
+3
View File
@@ -123,6 +123,9 @@ DB_DRIVER=postgres
# 向量存储类型(postgres/elasticsearch_v7/elasticsearch_v8/qdrant/milvus/weaviate)
RETRIEVE_DRIVER=postgres
# 允许用户使用哪些文件存储类型,使用逗号分隔,留空则允许所有类型的存储
# STORAGE_ALLOW_LIST=local,minio,cos,tos,s3
# 文件存储类型(local/minio/cos/tos/s3)
STORAGE_TYPE=local
+8 -6
View File
@@ -170,9 +170,9 @@ export function reconnectDocReader(addr: string): Promise<ParserEnginesResponse
export interface StorageEngineConfig {
default_provider: string // "local" | "minio" | "cos" | "tos" | "s3" | "oss"
local?: { path_prefix: string }
minio?: { mode: string; endpoint: string; access_key_id: string; secret_access_key: string; bucket_name: string; use_ssl: boolean; path_prefix: string }
cos?: {
local: { path_prefix: string }
minio: { mode: string; endpoint: string; access_key_id: string; secret_access_key: string; bucket_name: string; use_ssl: boolean; path_prefix: string }
cos: {
secret_id: string
secret_key: string
region: string
@@ -180,7 +180,7 @@ export interface StorageEngineConfig {
app_id: string
path_prefix: string
}
tos?: {
tos: {
endpoint: string
region: string
access_key: string
@@ -188,7 +188,7 @@ export interface StorageEngineConfig {
bucket_name: string
path_prefix: string
}
s3?: {
s3: {
endpoint: string
region: string
access_key: string
@@ -196,7 +196,7 @@ export interface StorageEngineConfig {
bucket_name: string
path_prefix: string
}
oss?: {
oss: {
endpoint: string
region: string
access_key: string
@@ -211,12 +211,14 @@ export interface StorageEngineConfig {
export interface StorageEngineStatusItem {
name: string
allowed?: boolean
available: boolean
description: string
}
export interface GetStorageEngineStatusResponse {
engines: StorageEngineStatusItem[]
allowed_providers?: string[]
minio_env_available: boolean
}
@@ -35,7 +35,8 @@
>
<span class="select-option">
<span>{{ opt.label }}</span>
<t-tag v-if="opt.disabled" theme="warning" variant="light" size="small">{{ $t('kbSettings.storage.notConfigured') }}</t-tag>
<t-tag v-if="opt.disabled && opt.allowed === false" theme="danger" variant="light" size="small">{{ $t('kbSettings.storage.unavailable') }}</t-tag>
<t-tag v-else-if="opt.disabled" theme="warning" variant="light" size="small">{{ $t('kbSettings.storage.notConfigured') }}</t-tag>
<t-tag v-else-if="opt.available === false" theme="danger" variant="light" size="small">{{ $t('kbSettings.storage.unavailable') }}</t-tag>
</span>
</t-option>
@@ -71,55 +72,64 @@ const localProvider = ref(props.storageProvider || 'local')
const loading = ref(true)
const engineStatus = ref<StorageEngineStatusItem[]>([])
const defaultProvider = ref('local')
const allowedProviders = ref<string[]>([])
const hasAnyConfig = ref(false)
const engineOptions = computed(() => {
const statusMap: Record<string, boolean> = {}
const allowedMap: Record<string, boolean> = {}
for (const e of engineStatus.value) {
statusMap[e.name] = e.available
allowedMap[e.name] = e.allowed !== false
}
return [
{
value: 'local',
label: t('kbSettings.storage.engineLocal'),
desc: t('kbSettings.storage.engineLocalDesc'),
allowed: allowedMap.local !== false,
available: statusMap.local !== false,
disabled: false,
disabled: allowedMap.local === false,
},
{
value: 'minio',
label: 'MinIO',
desc: t('kbSettings.storage.engineMinioDesc'),
allowed: allowedMap.minio !== false,
available: statusMap.minio,
disabled: statusMap.minio === false,
disabled: allowedMap.minio === false || statusMap.minio === false,
},
{
value: 'cos',
label: t('kbSettings.storage.engineCos'),
desc: t('kbSettings.storage.engineCosDesc'),
allowed: allowedMap.cos !== false,
available: statusMap.cos,
disabled: statusMap.cos === false,
disabled: allowedMap.cos === false || statusMap.cos === false,
},
{
value: 'tos',
label: t('kbSettings.storage.engineTos'),
desc: t('kbSettings.storage.engineTosDesc'),
allowed: allowedMap.tos !== false,
available: statusMap.tos,
disabled: statusMap.tos === false,
disabled: allowedMap.tos === false || statusMap.tos === false,
},
{
value: 's3',
label: t('kbSettings.storage.engineS3'),
desc: t('kbSettings.storage.engineS3Desc'),
allowed: allowedMap.s3 !== false,
available: statusMap.s3,
disabled: statusMap.s3 === false,
disabled: allowedMap.s3 === false || statusMap.s3 === false,
},
{
value: 'oss',
label: t('kbSettings.storage.engineOss'),
desc: t('kbSettings.storage.engineOssDesc'),
allowed: allowedMap.oss !== false,
available: statusMap.oss,
disabled: statusMap.oss === false,
disabled: allowedMap.oss === false || statusMap.oss === false,
},
]
})
@@ -136,6 +146,14 @@ function handleChange() {
emit('update:storageProvider', localProvider.value)
}
function ensureAllowedProvider() {
const current = engineOptions.value.find(o => o.value === localProvider.value && !o.disabled)
if (current) return
const fallback = engineOptions.value.find(o => !o.disabled)?.value || defaultProvider.value || 'local'
localProvider.value = fallback
emit('update:storageProvider', localProvider.value)
}
function goToStorageSettings() {
uiStore.closeKBEditor?.()
uiStore.openSettings?.('storage')
@@ -150,6 +168,7 @@ async function load() {
])
const engines = statusRes?.data?.engines ?? []
engineStatus.value = engines
allowedProviders.value = statusRes?.data?.allowed_providers ?? []
defaultProvider.value = configRes?.data?.default_provider || 'local'
const d = configRes?.data
hasAnyConfig.value = !!(d?.local?.path_prefix || d?.minio?.bucket_name || d?.cos?.bucket_name || d?.tos?.bucket_name || d?.s3?.bucket_name)
@@ -157,6 +176,7 @@ async function load() {
localProvider.value = defaultProvider.value
emit('update:storageProvider', localProvider.value)
}
ensureAllowedProvider()
} catch {
engineStatus.value = []
} finally {
@@ -32,14 +32,16 @@
v-model="config.default_provider"
style="width: 280px;"
:placeholder="$t('settings.storage.defaultEngine')"
:disabled="!hasAllowedProviders"
@change="onSaveDefaultEngine"
>
<t-option value="local" :label="$t('settings.storage.engineLocal')" />
<t-option value="minio" label="MinIO" />
<t-option value="cos" :label="$t('settings.storage.engineCos')" />
<t-option value="tos" :label="$t('settings.storage.engineTos')" />
<t-option value="s3" label="AWS S3" />
<t-option value="oss" :label="$t('settings.storage.engineOss')" />
<t-option
v-for="opt in providerOptions"
:key="opt.value"
:value="opt.value"
:label="opt.label"
:disabled="!opt.allowed"
/>
</t-select>
<span v-if="saveMessage && !drawerVisible" :class="['save-msg', saveSuccess ? 'success' : 'error']" style="margin-left: 12px;">
{{ saveMessage }}
@@ -49,8 +51,11 @@
</div>
<div class="engine-cards">
<!-- Local -->
<div :class="['engine-card', { active: drawerVisible && currentEngine === 'local' }]" @click="openDrawer('local')">
<div
v-if="isProviderAllowed('local')"
:class="['engine-card', { active: drawerVisible && currentEngine === 'local' }]"
@click="openDrawer('local')"
>
<div class="engine-card-header">
<h3>{{ $t('settings.storage.localTitle') }}</h3>
<t-tag theme="success" variant="light" size="small">{{ $t('settings.storage.available') }}</t-tag>
@@ -58,8 +63,11 @@
<p class="engine-card-desc">{{ $t('settings.storage.localDesc') }}</p>
</div>
<!-- MinIO -->
<div :class="['engine-card', { active: drawerVisible && currentEngine === 'minio' }]" @click="openDrawer('minio')">
<div
v-if="isProviderAllowed('minio')"
:class="['engine-card', { active: drawerVisible && currentEngine === 'minio' }]"
@click="openDrawer('minio')"
>
<div class="engine-card-header">
<h3>MinIO</h3>
<t-tag v-if="minioAvailable" theme="success" variant="light" size="small">{{ $t('settings.storage.available') }}</t-tag>
@@ -68,8 +76,11 @@
<p class="engine-card-desc">{{ $t('settings.storage.minioDesc') }}</p>
</div>
<!-- COS -->
<div :class="['engine-card', { active: drawerVisible && currentEngine === 'cos' }]" @click="openDrawer('cos')">
<div
v-if="isProviderAllowed('cos')"
:class="['engine-card', { active: drawerVisible && currentEngine === 'cos' }]"
@click="openDrawer('cos')"
>
<div class="engine-card-header">
<h3>{{ $t('settings.storage.cosTitle') }}</h3>
<t-tag theme="success" variant="light" size="small">{{ $t('settings.storage.configurable') }}</t-tag>
@@ -77,8 +88,11 @@
<p class="engine-card-desc">{{ $t('settings.storage.cosDesc') }}</p>
</div>
<!-- TOS -->
<div :class="['engine-card', { active: drawerVisible && currentEngine === 'tos' }]" @click="openDrawer('tos')">
<div
v-if="isProviderAllowed('tos')"
:class="['engine-card', { active: drawerVisible && currentEngine === 'tos' }]"
@click="openDrawer('tos')"
>
<div class="engine-card-header">
<h3>{{ $t('settings.storage.tosTitle') }}</h3>
<t-tag theme="success" variant="light" size="small">{{ $t('settings.storage.configurable') }}</t-tag>
@@ -86,8 +100,11 @@
<p class="engine-card-desc">{{ $t('settings.storage.tosDesc') }}</p>
</div>
<!-- S3 -->
<div :class="['engine-card', { active: drawerVisible && currentEngine === 's3' }]" @click="openDrawer('s3')">
<div
v-if="isProviderAllowed('s3')"
:class="['engine-card', { active: drawerVisible && currentEngine === 's3' }]"
@click="openDrawer('s3')"
>
<div class="engine-card-header">
<h3>{{ $t('settings.storage.s3Title') }}</h3>
<t-tag theme="success" variant="light" size="small">{{ $t('settings.storage.configurable') }}</t-tag>
@@ -95,8 +112,11 @@
<p class="engine-card-desc">{{ $t('settings.storage.s3Desc') }}</p>
</div>
<!-- OSS -->
<div :class="['engine-card', { active: drawerVisible && currentEngine === 'oss' }]" @click="openDrawer('oss')">
<div
v-if="isProviderAllowed('oss')"
:class="['engine-card', { active: drawerVisible && currentEngine === 'oss' }]"
@click="openDrawer('oss')"
>
<div class="engine-card-header">
<h3>{{ $t('settings.storage.ossTitle') }}</h3>
<t-tag theme="success" variant="light" size="small">{{ $t('settings.storage.configurable') }}</t-tag>
@@ -106,7 +126,6 @@
</div>
</template>
<!-- 配置抽屉 -->
<t-drawer
v-model:visible="drawerVisible"
:header="drawerTitle"
@@ -115,7 +134,6 @@
@confirm="onSave"
>
<div class="drawer-content">
<!-- Local -->
<template v-if="currentEngine === 'local'">
<div class="engine-info-block">
<p class="engine-desc">{{ $t('settings.storage.localDesc') }}</p>
@@ -132,7 +150,6 @@
</div>
</template>
<!-- MinIO -->
<template v-else-if="currentEngine === 'minio'">
<div class="engine-info-block">
<p class="engine-desc">{{ $t('settings.storage.minioDesc') }}</p>
@@ -154,7 +171,6 @@
</div>
</div>
<!-- Docker mode -->
<div v-if="config.minio.mode !== 'remote'">
<div v-if="minioEnvAvailable" class="engine-hint success">
{{ $t('settings.storage.minioDockerDetected') }}
@@ -187,42 +203,24 @@
</div>
</div>
<!-- Remote mode -->
<div v-else>
<div class="engine-hint">{{ $t('settings.storage.minioRemoteHint') }}</div>
<div class="engine-form">
<div class="form-item">
<label class="form-label">Endpoint</label>
<t-input
v-model="config.minio.endpoint"
placeholder="e.g. minio.example.com:9000"
clearable
/>
<t-input v-model="config.minio.endpoint" placeholder="e.g. minio.example.com:9000" clearable />
</div>
<div class="form-item">
<label class="form-label">Access Key ID</label>
<t-input
v-model="config.minio.access_key_id"
placeholder="MinIO Access Key"
clearable
/>
<t-input v-model="config.minio.access_key_id" placeholder="MinIO Access Key" clearable />
</div>
<div class="form-item">
<label class="form-label">Secret Access Key</label>
<t-input
v-model="config.minio.secret_access_key"
type="password"
placeholder="MinIO Secret Key"
clearable
/>
<t-input v-model="config.minio.secret_access_key" type="password" placeholder="MinIO Secret Key" clearable />
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.bucketName') }}</label>
<t-input
v-model="config.minio.bucket_name"
:placeholder="$t('settings.storage.bucketPlaceholder')"
clearable
/>
<t-input v-model="config.minio.bucket_name" :placeholder="$t('settings.storage.bucketPlaceholder')" clearable />
</div>
<div class="form-item form-item--inline">
<label class="form-label">Use SSL</label>
@@ -230,17 +228,12 @@
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.pathPrefix') }}</label>
<t-input
v-model="config.minio.path_prefix"
:placeholder="$t('settings.storage.prefixPlaceholder')"
clearable
/>
<t-input v-model="config.minio.path_prefix" :placeholder="$t('settings.storage.prefixPlaceholder')" clearable />
</div>
</div>
</div>
</template>
<!-- COS -->
<template v-else-if="currentEngine === 'cos'">
<div class="engine-info-block">
<p class="engine-desc">
@@ -252,57 +245,31 @@
<div class="engine-form">
<div class="form-item">
<label class="form-label">Secret ID</label>
<t-input
v-model="config.cos.secret_id"
:placeholder="$t('settings.storage.cosSecretIdPlaceholder')"
clearable
/>
<t-input v-model="config.cos.secret_id" :placeholder="$t('settings.storage.cosSecretIdPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label">Secret Key</label>
<t-input
v-model="config.cos.secret_key"
type="password"
:placeholder="$t('settings.storage.cosSecretKeyPlaceholder')"
clearable
/>
<t-input v-model="config.cos.secret_key" type="password" :placeholder="$t('settings.storage.cosSecretKeyPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label">Region</label>
<t-input
v-model="config.cos.region"
placeholder="e.g. ap-guangzhou"
clearable
/>
<t-input v-model="config.cos.region" placeholder="e.g. ap-guangzhou" clearable />
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.bucketName') }}</label>
<t-input
v-model="config.cos.bucket_name"
:placeholder="$t('settings.storage.bucketPlaceholder')"
clearable
/>
<t-input v-model="config.cos.bucket_name" :placeholder="$t('settings.storage.bucketPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label">App ID</label>
<t-input
v-model="config.cos.app_id"
:placeholder="$t('settings.storage.cosAppIdPlaceholder')"
clearable
/>
<t-input v-model="config.cos.app_id" :placeholder="$t('settings.storage.cosAppIdPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.pathPrefix') }}</label>
<t-input
v-model="config.cos.path_prefix"
:placeholder="$t('settings.storage.prefixPlaceholder')"
clearable
/>
<t-input v-model="config.cos.path_prefix" :placeholder="$t('settings.storage.prefixPlaceholder')" clearable />
</div>
</div>
</template>
<!-- TOS -->
<template v-else-if="currentEngine === 'tos'">
<div class="engine-info-block">
<p class="engine-desc">
@@ -314,57 +281,31 @@
<div class="engine-form">
<div class="form-item">
<label class="form-label">Endpoint</label>
<t-input
v-model="config.tos.endpoint"
placeholder="e.g. https://tos-cn-beijing.volces.com"
clearable
/>
<t-input v-model="config.tos.endpoint" placeholder="e.g. https://tos-cn-beijing.volces.com" clearable />
</div>
<div class="form-item">
<label class="form-label">Region</label>
<t-input
v-model="config.tos.region"
placeholder="e.g. cn-beijing"
clearable
/>
<t-input v-model="config.tos.region" placeholder="e.g. cn-beijing" clearable />
</div>
<div class="form-item">
<label class="form-label">Access Key</label>
<t-input
v-model="config.tos.access_key"
:placeholder="$t('settings.storage.tosAccessKeyPlaceholder')"
clearable
/>
<t-input v-model="config.tos.access_key" :placeholder="$t('settings.storage.tosAccessKeyPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label">Secret Key</label>
<t-input
v-model="config.tos.secret_key"
type="password"
:placeholder="$t('settings.storage.tosSecretKeyPlaceholder')"
clearable
/>
<t-input v-model="config.tos.secret_key" type="password" :placeholder="$t('settings.storage.tosSecretKeyPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.bucketName') }}</label>
<t-input
v-model="config.tos.bucket_name"
:placeholder="$t('settings.storage.bucketPlaceholder')"
clearable
/>
<t-input v-model="config.tos.bucket_name" :placeholder="$t('settings.storage.bucketPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.pathPrefix') }}</label>
<t-input
v-model="config.tos.path_prefix"
:placeholder="$t('settings.storage.prefixPlaceholder')"
clearable
/>
<t-input v-model="config.tos.path_prefix" :placeholder="$t('settings.storage.prefixPlaceholder')" clearable />
</div>
</div>
</template>
<!-- S3 -->
<template v-else-if="currentEngine === 's3'">
<div class="engine-info-block">
<p class="engine-desc">
@@ -376,57 +317,31 @@
<div class="engine-form">
<div class="form-item">
<label class="form-label">Endpoint</label>
<t-input
v-model="config.s3.endpoint"
placeholder="e.g. https://s3.amazonaws.com"
clearable
/>
<t-input v-model="config.s3.endpoint" placeholder="e.g. https://s3.amazonaws.com" clearable />
</div>
<div class="form-item">
<label class="form-label">Region</label>
<t-input
v-model="config.s3.region"
placeholder="e.g. us-east-1"
clearable
/>
<t-input v-model="config.s3.region" placeholder="e.g. us-east-1" clearable />
</div>
<div class="form-item">
<label class="form-label">Access Key</label>
<t-input
v-model="config.s3.access_key"
:placeholder="$t('settings.storage.s3AccessKeyPlaceholder')"
clearable
/>
<t-input v-model="config.s3.access_key" :placeholder="$t('settings.storage.s3AccessKeyPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label">Secret Key</label>
<t-input
v-model="config.s3.secret_key"
type="password"
:placeholder="$t('settings.storage.s3SecretKeyPlaceholder')"
clearable
/>
<t-input v-model="config.s3.secret_key" type="password" :placeholder="$t('settings.storage.s3SecretKeyPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.bucketName') }}</label>
<t-input
v-model="config.s3.bucket_name"
:placeholder="$t('settings.storage.bucketPlaceholder')"
clearable
/>
<t-input v-model="config.s3.bucket_name" :placeholder="$t('settings.storage.bucketPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.pathPrefix') }}</label>
<t-input
v-model="config.s3.path_prefix"
:placeholder="$t('settings.storage.prefixPlaceholder')"
clearable
/>
<t-input v-model="config.s3.path_prefix" :placeholder="$t('settings.storage.prefixPlaceholder')" clearable />
</div>
</div>
</template>
<!-- OSS -->
<template v-else-if="currentEngine === 'oss'">
<div class="engine-info-block">
<p class="engine-desc">
@@ -438,55 +353,31 @@
<div class="engine-form">
<div class="form-item">
<label class="form-label">Endpoint</label>
<t-input
v-model="config.oss.endpoint"
placeholder="e.g. https://oss-cn-hangzhou.aliyuncs.com"
clearable
/>
<t-input v-model="config.oss.endpoint" placeholder="e.g. https://oss-cn-hangzhou.aliyuncs.com" clearable />
</div>
<div class="form-item">
<label class="form-label">Region</label>
<t-input
v-model="config.oss.region"
placeholder="e.g. cn-hangzhou"
clearable
/>
<t-input v-model="config.oss.region" placeholder="e.g. cn-hangzhou" clearable />
</div>
<div class="form-item">
<label class="form-label">Access Key</label>
<t-input
v-model="config.oss.access_key"
:placeholder="$t('settings.storage.ossAccessKeyPlaceholder')"
clearable
/>
<t-input v-model="config.oss.access_key" :placeholder="$t('settings.storage.ossAccessKeyPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label">Secret Key</label>
<t-input
v-model="config.oss.secret_key"
type="password"
:placeholder="$t('settings.storage.ossSecretKeyPlaceholder')"
clearable
/>
<t-input v-model="config.oss.secret_key" type="password" :placeholder="$t('settings.storage.ossSecretKeyPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.bucketName') }}</label>
<t-input
v-model="config.oss.bucket_name"
:placeholder="$t('settings.storage.bucketPlaceholder')"
clearable
/>
<t-input v-model="config.oss.bucket_name" :placeholder="$t('settings.storage.bucketPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.pathPrefix') }}</label>
<t-input
v-model="config.oss.path_prefix"
:placeholder="$t('settings.storage.prefixPlaceholder')"
clearable
/>
<t-input v-model="config.oss.path_prefix" :placeholder="$t('settings.storage.prefixPlaceholder')" clearable />
</div>
</div>
</template>
<div class="form-item" v-if="currentEngine && currentEngine !== 'local'">
<label class="form-label">{{ $t('settings.storage.testConnection') }}</label>
<div class="api-test-section">
@@ -511,13 +402,13 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import {
getStorageEngineConfig,
updateStorageEngineConfig,
getStorageEngineStatus,
checkStorageEngine,
getStorageEngineConfig,
getStorageEngineStatus,
updateStorageEngineConfig,
type StorageEngineConfig,
} from '@/api/system'
@@ -527,30 +418,9 @@ const defaultConfig = (): StorageEngineConfig => ({
default_provider: 'local',
local: { path_prefix: '' },
minio: { mode: 'docker', endpoint: '', access_key_id: '', secret_access_key: '', bucket_name: '', use_ssl: false, path_prefix: '' },
cos: {
secret_id: '',
secret_key: '',
region: '',
bucket_name: '',
app_id: '',
path_prefix: '',
},
tos: {
endpoint: '',
region: '',
access_key: '',
secret_key: '',
bucket_name: '',
path_prefix: '',
},
s3: {
endpoint: '',
region: '',
access_key: '',
secret_key: '',
bucket_name: '',
path_prefix: '',
},
cos: { secret_id: '', secret_key: '', region: '', bucket_name: '', app_id: '', path_prefix: '' },
tos: { endpoint: '', region: '', access_key: '', secret_key: '', bucket_name: '', path_prefix: '' },
s3: { endpoint: '', region: '', access_key: '', secret_key: '', bucket_name: '', path_prefix: '' },
oss: {
endpoint: '',
region: '',
@@ -567,11 +437,8 @@ const defaultConfig = (): StorageEngineConfig => ({
const loading = ref(true)
const error = ref('')
const config = ref<StorageEngineConfig>(defaultConfig())
const engineStatus = ref<{ local: boolean; minio: boolean; cos: boolean }>({
local: true,
minio: false,
cos: true,
})
const allowedProviders = ref<string[] | null>(null)
const engineStatus = ref<{ local: boolean; minio: boolean; cos: boolean }>({ local: true, minio: false, cos: true })
const minioEnvAvailable = ref(false)
const saving = ref(false)
const saveMessage = ref('')
@@ -591,14 +458,31 @@ const ossCheckResult = ref<{ ok: boolean; message: string } | null>(null)
const drawerVisible = ref(false)
const currentEngine = ref<string | null>(null)
const providerOptions = computed(() => [
{ value: 'local', label: t('settings.storage.engineLocal'), allowed: isProviderAllowed('local') },
{ value: 'minio', label: 'MinIO', allowed: isProviderAllowed('minio') },
{ value: 'cos', label: t('settings.storage.engineCos'), allowed: isProviderAllowed('cos') },
{ value: 'tos', label: t('settings.storage.engineTos'), allowed: isProviderAllowed('tos') },
{ value: 's3', label: 'AWS S3', allowed: isProviderAllowed('s3') },
{ value: 'oss', label: t('settings.storage.engineOss'), allowed: isProviderAllowed('oss') },
])
const hasAllowedProviders = computed(() => (allowedProviders.value?.length ?? 0) > 0)
const currentCheckState = computed(() => {
switch (currentEngine.value) {
case 'minio': return { loading: checkingMinio.value, result: minioCheckResult.value, onCheck: onCheckMinio }
case 'cos': return { loading: checkingCos.value, result: cosCheckResult.value, onCheck: onCheckCos }
case 'tos': return { loading: checkingTos.value, result: tosCheckResult.value, onCheck: onCheckTos }
case 's3': return { loading: checkingS3.value, result: s3CheckResult.value, onCheck: onCheckS3 }
case 'oss': return { loading: checkingOss.value, result: ossCheckResult.value, onCheck: onCheckOss }
default: return { loading: false, result: null, onCheck: () => {} }
case 'minio':
return { loading: checkingMinio.value, result: minioCheckResult.value, onCheck: onCheckMinio }
case 'cos':
return { loading: checkingCos.value, result: cosCheckResult.value, onCheck: onCheckCos }
case 'tos':
return { loading: checkingTos.value, result: tosCheckResult.value, onCheck: onCheckTos }
case 's3':
return { loading: checkingS3.value, result: s3CheckResult.value, onCheck: onCheckS3 }
case 'oss':
return { loading: checkingOss.value, result: ossCheckResult.value, onCheck: onCheckOss }
default:
return { loading: false, result: null, onCheck: () => undefined }
}
})
@@ -622,12 +506,21 @@ const minioAvailable = computed(() => {
return minioEnvAvailable.value
})
function isProviderAllowed(provider: string) {
if (allowedProviders.value === null) return true
return allowedProviders.value.includes(provider)
}
function ensureAllowedDefaultProvider() {
if (isProviderAllowed(config.value.default_provider)) return
config.value.default_provider = allowedProviders.value?.[0] || 'local'
}
function openDrawer(engine: string) {
if (!isProviderAllowed(engine)) return
currentEngine.value = engine
drawerVisible.value = true
saveMessage.value = ''
// Reset check results
minioCheckResult.value = null
cosCheckResult.value = null
tosCheckResult.value = null
@@ -653,7 +546,7 @@ async function loadConfig() {
use_ssl: d.minio.use_ssl ?? false,
path_prefix: d.minio.path_prefix || '',
}
: defaultConfig().minio!,
: defaultConfig().minio,
cos: d.cos
? {
secret_id: d.cos.secret_id || '',
@@ -663,7 +556,7 @@ async function loadConfig() {
app_id: d.cos.app_id || '',
path_prefix: d.cos.path_prefix || '',
}
: defaultConfig().cos!,
: defaultConfig().cos,
tos: d.tos
? {
endpoint: d.tos.endpoint || '',
@@ -673,7 +566,7 @@ async function loadConfig() {
bucket_name: d.tos.bucket_name || '',
path_prefix: d.tos.path_prefix || '',
}
: defaultConfig().tos!,
: defaultConfig().tos,
s3: d.s3
? {
endpoint: d.s3.endpoint || '',
@@ -683,7 +576,7 @@ async function loadConfig() {
bucket_name: d.s3.bucket_name || '',
path_prefix: d.s3.path_prefix || '',
}
: defaultConfig().s3!,
: defaultConfig().s3,
oss: d.oss
? {
endpoint: d.oss.endpoint || '',
@@ -696,7 +589,7 @@ async function loadConfig() {
temp_bucket_name: d.oss.temp_bucket_name || '',
temp_region: d.oss.temp_region || '',
}
: defaultConfig().oss!,
: defaultConfig().oss,
}
}
} catch {
@@ -708,6 +601,9 @@ async function loadStatus() {
try {
const res = await getStorageEngineStatus()
const engines = res?.data?.engines ?? []
allowedProviders.value = res?.data?.allowed_providers?.length
? res.data.allowed_providers
: engines.filter(e => e.allowed !== false).map(e => e.name)
const status = { local: true, minio: false, cos: true }
for (const e of engines) {
if (e.name === 'local') status.local = e.available
@@ -718,6 +614,7 @@ async function loadStatus() {
minioEnvAvailable.value = res?.data?.minio_env_available ?? false
} catch {
engineStatus.value = { local: true, minio: false, cos: true }
allowedProviders.value = ['local', 'minio', 'cos', 'tos', 's3', 'oss']
minioEnvAvailable.value = false
}
}
@@ -727,6 +624,7 @@ async function loadAll() {
error.value = ''
try {
await Promise.all([loadConfig(), loadStatus()])
ensureAllowedDefaultProvider()
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : t('settings.storage.loadFailed')
} finally {
@@ -779,7 +677,6 @@ function buildPayload(): StorageEngineConfig {
secret_key: (config.value.oss?.secret_key || '').trim(),
bucket_name: (config.value.oss?.bucket_name || '').trim(),
path_prefix: (config.value.oss?.path_prefix || '').trim(),
// Temp bucket fields: not exposed in UI; server manages these independently
use_temp_bucket: config.value.oss?.use_temp_bucket ?? false,
temp_bucket_name: (config.value.oss?.temp_bucket_name || '').trim(),
temp_region: (config.value.oss?.temp_region || '').trim(),
@@ -791,8 +688,10 @@ async function onSave() {
saving.value = true
saveMessage.value = ''
try {
ensureAllowedDefaultProvider()
await updateStorageEngineConfig(buildPayload())
await loadStatus()
ensureAllowedDefaultProvider()
saveSuccess.value = true
saveMessage.value = t('settings.storage.saveSuccess')
drawerVisible.value = false
@@ -965,7 +864,6 @@ onMounted(loadAll)
align-items: center;
}
// ---- 引擎卡片布局 ----
.engine-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
@@ -1024,7 +922,6 @@ onMounted(loadAll)
overflow: hidden;
}
// ---- 抽屉内容 ----
.drawer-content {
display: flex;
flex-direction: column;
@@ -1040,7 +937,6 @@ onMounted(loadAll)
}
}
// 输入框样式
:deep(.t-input),
:deep(.t-select) {
width: 100%;
+4
View File
@@ -333,6 +333,10 @@ func (h *InitializationHandler) UpdateKBConfig(c *gin.Context) {
if provider == "" {
provider = "local"
}
if !isStorageProviderAllowed(provider) {
c.Error(errors.NewBadRequestError("Storage provider is not allowed by STORAGE_ALLOW_LIST"))
return
}
oldProvider := kb.GetStorageProvider()
if oldProvider == "" {
oldProvider = "local"
+6
View File
@@ -5,6 +5,7 @@ import (
stderrors "errors"
"net/http"
"strconv"
"strings"
"time"
"github.com/Tencent/WeKnora/internal/agent/tools"
@@ -130,6 +131,11 @@ func (h *KnowledgeBaseHandler) CreateKnowledgeBase(c *gin.Context) {
c.Error(err)
return
}
provider := strings.ToLower(strings.TrimSpace(req.GetStorageProvider()))
if provider != "" && !isStorageProviderAllowed(provider) {
c.Error(apperrors.NewBadRequestError("Storage provider is not allowed by STORAGE_ALLOW_LIST"))
return
}
logger.Infof(ctx, "Creating knowledge base, name: %s", secutils.SanitizeForLog(req.Name))
// Create knowledge base using the service
+68
View File
@@ -0,0 +1,68 @@
package handler
import (
"os"
"strings"
)
const storageAllowListEnv = "STORAGE_ALLOW_LIST"
var supportedStorageProviders = []string{"local", "minio", "cos", "tos", "s3", "oss"}
func getSupportedStorageProviders() []string {
providers := make([]string, len(supportedStorageProviders))
copy(providers, supportedStorageProviders)
return providers
}
func getAllowedStorageProviders() map[string]bool {
raw := strings.TrimSpace(os.Getenv(storageAllowListEnv))
allowed := make(map[string]bool, len(supportedStorageProviders))
if raw == "" {
for _, provider := range supportedStorageProviders {
allowed[provider] = true
}
return allowed
}
for _, item := range strings.FieldsFunc(raw, func(r rune) bool {
switch r {
case ',', ';', '|', '\n', '\t', ' ':
return true
default:
return false
}
}) {
provider := strings.ToLower(strings.TrimSpace(item))
if provider == "" {
continue
}
for _, supported := range supportedStorageProviders {
if provider == supported {
allowed[provider] = true
break
}
}
}
return allowed
}
func isStorageProviderAllowed(provider string) bool {
provider = strings.ToLower(strings.TrimSpace(provider))
if provider == "" {
return true
}
return getAllowedStorageProviders()[provider]
}
func firstAllowedStorageProvider() string {
allowed := getAllowedStorageProviders()
for _, provider := range supportedStorageProviders {
if allowed[provider] {
return provider
}
}
return ""
}
+31 -6
View File
@@ -444,6 +444,7 @@ func (h *SystemHandler) isTOSEnvAvailable() bool {
// StorageEngineStatusItem describes one storage engine's availability and description.
type StorageEngineStatusItem struct {
Name string `json:"name"` // "local", "minio", "cos", "tos"
Allowed bool `json:"allowed"`
Available bool `json:"available"` // whether the engine can be used
Description string `json:"description"` // short description for UI
}
@@ -451,6 +452,7 @@ type StorageEngineStatusItem struct {
// GetStorageEngineStatusResponse is the response for GET /system/storage-engine-status.
type GetStorageEngineStatusResponse struct {
Engines []StorageEngineStatusItem `json:"engines"`
AllowedProviders []string `json:"allowed_providers"`
MinioEnvAvailable bool `json:"minio_env_available"`
}
@@ -466,18 +468,27 @@ func (h *SystemHandler) GetStorageEngineStatus(c *gin.Context) {
minioEnvAvailable := h.isMinioEnvAvailable()
cosConfigured := h.isCOSConfigured(c)
tosConfigured := h.isTOSConfigured(c)
s3Configured := h.isS3Configured(c)
ossConfigured := h.isOSSConfigured(c)
allowed := getAllowedStorageProviders()
allowedProviders := make([]string, 0, len(supportedStorageProviders))
for _, provider := range getSupportedStorageProviders() {
if allowed[provider] {
allowedProviders = append(allowedProviders, provider)
}
}
engines := []StorageEngineStatusItem{
{Name: "local", Available: true, Description: "本地文件系统存储,仅适合单机部署"},
{Name: "minio", Available: minioConfigured || minioEnvAvailable, Description: "S3 兼容的自托管对象存储,适合内网和私有云部署"},
{Name: "cos", Available: cosConfigured, Description: "腾讯云对象存储服务,适合公有云部署,支持 CDN 加速"},
{Name: "tos", Available: tosConfigured, Description: "火山引擎对象存储服务,适合公有云部署"},
{Name: "oss", Available: ossConfigured, Description: "阿里云对象存储服务,适合公有云部署,支持 S3 兼容协议"},
{Name: "local", Allowed: allowed["local"], Available: true, Description: "本地文件系统存储,仅适合单机部署"},
{Name: "minio", Allowed: allowed["minio"], Available: minioConfigured || minioEnvAvailable, Description: "S3 兼容的自托管对象存储,适合内网和私有云部署"},
{Name: "cos", Allowed: allowed["cos"], Available: cosConfigured, Description: "腾讯云对象存储服务,适合公有云部署,支持 CDN 加速"},
{Name: "tos", Allowed: allowed["tos"], Available: tosConfigured, Description: "火山引擎对象存储服务,适合公有云部署"},
{Name: "s3", Allowed: allowed["s3"], Available: s3Configured, Description: "AWS S3 与兼容对象存储服务,适合公有云与混合云部署"},
{Name: "oss", Allowed: allowed["oss"], Available: ossConfigured, Description: "阿里云对象存储服务,适合公有云部署,支持 S3 兼容协议"},
}
c.JSON(200, gin.H{
"code": 0,
"msg": "success",
"data": GetStorageEngineStatusResponse{Engines: engines, MinioEnvAvailable: minioEnvAvailable},
"data": GetStorageEngineStatusResponse{Engines: engines, AllowedProviders: allowedProviders, MinioEnvAvailable: minioEnvAvailable},
})
}
@@ -607,6 +618,10 @@ func (h *SystemHandler) CheckStorageEngine(c *gin.Context) {
c.JSON(400, gin.H{"code": 1, "msg": "请求体格式错误"})
return
}
if !isStorageProviderAllowed(req.Provider) {
c.JSON(403, gin.H{"code": 1, "msg": "该存储引擎已被禁用"})
return
}
switch req.Provider {
case "minio":
@@ -624,6 +639,16 @@ func (h *SystemHandler) CheckStorageEngine(c *gin.Context) {
}
}
func (h *SystemHandler) isS3Configured(c *gin.Context) bool {
if v, exists := c.Get(types.TenantInfoContextKey.String()); exists {
if tenant, ok := v.(*types.Tenant); ok && tenant != nil && tenant.StorageEngineConfig != nil && tenant.StorageEngineConfig.S3 != nil {
s3Conf := tenant.StorageEngineConfig.S3
return s3Conf.Endpoint != "" && s3Conf.Region != "" && s3Conf.AccessKey != "" && s3Conf.SecretKey != "" && s3Conf.BucketName != ""
}
}
return false
}
func (h *SystemHandler) checkMinio(c *gin.Context, ctx context.Context, cfg *types.MinIOEngineConfig) {
if cfg == nil {
c.JSON(200, gin.H{"code": 0, "data": StorageCheckResponse{OK: false, Message: "未提供 MinIO 配置"}})
+14
View File
@@ -3,6 +3,7 @@ package handler
import (
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
@@ -842,6 +843,19 @@ func (h *TenantHandler) updateTenantStorageEngineConfigInternal(c *gin.Context)
c.Error(errors.NewValidationError("Invalid request data").WithDetails(err.Error()))
return
}
provider := strings.ToLower(strings.TrimSpace(cfg.DefaultProvider))
if provider == "" {
provider = firstAllowedStorageProvider()
}
if provider == "" {
c.Error(errors.NewBadRequestError("No storage provider is allowed by STORAGE_ALLOW_LIST"))
return
}
if !isStorageProviderAllowed(provider) {
c.Error(errors.NewBadRequestError("Storage provider is not allowed by STORAGE_ALLOW_LIST"))
return
}
cfg.DefaultProvider = provider
tenant, _ := types.TenantInfoFromContext(ctx)
if tenant == nil {
logger.Error(ctx, "Tenant is empty")