feat: 支持列出MinIO 存储桶及权限

This commit is contained in:
wizardchen
2025-12-30 11:37:30 +08:00
committed by lyingbug
parent 1f653ee5ad
commit f9593fc050
6 changed files with 220 additions and 6 deletions
+14
View File
@@ -96,3 +96,17 @@ export function updateConversationConfig(config: ConversationConfig): Promise<{
export function getPromptTemplates(): Promise<{ data: PromptTemplatesConfig }> {
return get('/api/v1/tenants/kv/prompt-templates')
}
export interface MinioBucketInfo {
name: string
policy: 'public' | 'private' | 'custom'
created_at?: string
}
export interface ListMinioBucketsResponse {
buckets: MinioBucketInfo[]
}
export function listMinioBuckets(): Promise<{ data: ListMinioBucketsResponse }> {
return get('/api/v1/system/minio/buckets')
}
+7 -1
View File
@@ -1270,7 +1270,13 @@ export default {
minio: {
bucketLabel: 'Bucket Name',
bucketDescription: 'Name of the MinIO bucket (required)',
bucketPlaceholder: 'Enter bucket name (required)',
bucketPlaceholder: 'Select or enter bucket name',
bucketHint: 'Select an existing bucket with public read access, or enter a new name to create automatically',
policyLabels: {
public: 'Public Read',
private: 'Private',
custom: 'Custom'
},
useSslLabel: 'Use SSL',
useSslDescription: 'Whether to use SSL connection',
pathPrefixLabel: 'Path Prefix',
+7 -1
View File
@@ -1621,7 +1621,13 @@ export default {
minio: {
bucketLabel: "Bucket 名称",
bucketDescription: "MinIO 存储桶名称(必填)",
bucketPlaceholder: "请输入 Bucket 名称(必填)",
bucketPlaceholder: "选择或输入 Bucket 名称",
bucketHint: "选择已存在的公有读权限 Bucket,或输入新名称将自动创建",
policyLabels: {
public: "公有读",
private: "私有",
custom: "自定义"
},
useSslLabel: "使用 SSL",
useSslDescription: "是否使用 SSL 连接",
pathPrefixLabel: "路径前缀",
@@ -110,14 +110,51 @@
<div class="setting-info">
<label>{{ $t('knowledgeEditor.advanced.multimodal.minio.bucketLabel') }} <span class="required">*</span></label>
<p class="desc">{{ $t('knowledgeEditor.advanced.multimodal.minio.bucketDescription') }}</p>
<t-alert
theme="info"
:message="$t('knowledgeEditor.advanced.multimodal.minio.bucketHint')"
style="margin-top: 8px;"
/>
</div>
<div class="setting-control">
<t-input
<div class="setting-control bucket-control">
<t-select
v-model="localMultimodal.minio.bucketName"
:placeholder="$t('knowledgeEditor.advanced.multimodal.minio.bucketPlaceholder')"
:loading="loadingBuckets"
filterable
creatable
@change="handleConfigChange"
@focus="loadMinioBuckets"
style="width: 280px;"
/>
>
<t-option
v-for="bucket in minioBuckets"
:key="bucket.name"
:value="bucket.name"
:label="bucket.name"
>
<div class="bucket-option">
<span class="bucket-name">{{ bucket.name }}</span>
<t-tag
:theme="bucket.policy === 'public' ? 'success' : bucket.policy === 'private' ? 'default' : 'warning'"
size="small"
variant="light"
>
{{ $t(`knowledgeEditor.advanced.multimodal.minio.policyLabels.${bucket.policy}`) }}
</t-tag>
</div>
</t-option>
</t-select>
<t-button
theme="default"
variant="outline"
size="small"
:loading="loadingBuckets"
@click="loadMinioBuckets"
style="margin-left: 8px;"
>
<template #icon><refresh-icon /></template>
</t-button>
</div>
</div>
@@ -252,9 +289,10 @@
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { RefreshIcon } from 'tdesign-icons-vue-next'
import ModelSelector from '@/components/ModelSelector.vue'
import { useUIStore } from '@/stores/ui'
import { getSystemInfo } from '@/api/system'
import { getSystemInfo, listMinioBuckets, type MinioBucketInfo } from '@/api/system'
const uiStore = useUIStore()
@@ -302,6 +340,25 @@ const localQuestionGeneration = ref<QuestionGenerationConfig>(
const vllmSelectorRef = ref()
const isMinioEnabled = ref(false)
const minioBuckets = ref<MinioBucketInfo[]>([])
const loadingBuckets = ref(false)
// Load MinIO buckets
const loadMinioBuckets = async () => {
if (!isMinioEnabled.value || loadingBuckets.value) return
loadingBuckets.value = true
try {
const response = await listMinioBuckets()
if (response.data?.buckets) {
minioBuckets.value = response.data.buckets
}
} catch (error) {
console.error('Failed to load MinIO buckets:', error)
} finally {
loadingBuckets.value = false
}
}
// Check system status on mount
onMounted(async () => {
@@ -316,6 +373,11 @@ onMounted(async () => {
localMultimodal.value.storageType = 'cos'
emit('update:multimodal', localMultimodal.value)
}
// Load MinIO buckets if enabled
if (isMinioEnabled.value) {
loadMinioBuckets()
}
} catch (error) {
console.error('Failed to fetch system info:', error)
// Default to disabled if we can't fetch the info
@@ -513,6 +575,25 @@ const handleConfigChange = () => {
margin-top: 8px;
}
.bucket-control {
display: flex;
align-items: center;
}
.bucket-option {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.bucket-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.config-item {
background: #ffffff;
border: 1px solid #e5e7eb;
+106
View File
@@ -1,12 +1,15 @@
package handler
import (
"context"
"os"
"strings"
"github.com/Tencent/WeKnora/internal/config"
"github.com/Tencent/WeKnora/internal/logger"
"github.com/gin-gonic/gin"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/neo4j/neo4j-go-driver/v6/neo4j"
)
@@ -155,3 +158,106 @@ func (h *SystemHandler) isMinioEnabled() bool {
return endpoint != "" && accessKeyID != "" && secretAccessKey != ""
}
// MinioBucketInfo represents bucket information with access policy
type MinioBucketInfo struct {
Name string `json:"name"`
Policy string `json:"policy"` // "public", "private", "custom"
CreatedAt string `json:"created_at,omitempty"`
}
// ListMinioBucketsResponse defines the response structure for listing buckets
type ListMinioBucketsResponse struct {
Buckets []MinioBucketInfo `json:"buckets"`
}
// ListMinioBuckets godoc
// @Summary 列出 MinIO 存储桶
// @Description 获取所有 MinIO 存储桶及其访问权限
// @Tags 系统
// @Accept json
// @Produce json
// @Success 200 {object} ListMinioBucketsResponse "存储桶列表"
// @Failure 400 {object} map[string]interface{} "MinIO 未启用"
// @Failure 500 {object} map[string]interface{} "服务器错误"
// @Router /system/minio/buckets [get]
func (h *SystemHandler) ListMinioBuckets(c *gin.Context) {
ctx := logger.CloneContext(c.Request.Context())
// Check if MinIO is enabled
if !h.isMinioEnabled() {
logger.Warn(ctx, "MinIO is not enabled")
c.JSON(400, gin.H{
"code": 400,
"msg": "MinIO is not enabled",
"success": false,
})
return
}
// Get MinIO configuration from environment
endpoint := os.Getenv("MINIO_ENDPOINT")
accessKeyID := os.Getenv("MINIO_ACCESS_KEY_ID")
secretAccessKey := os.Getenv("MINIO_SECRET_ACCESS_KEY")
useSSL := os.Getenv("MINIO_USE_SSL") == "true"
// Create MinIO client
minioClient, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
Secure: useSSL,
})
if err != nil {
logger.Error(ctx, "Failed to create MinIO client", "error", err)
c.JSON(500, gin.H{
"code": 500,
"msg": "Failed to connect to MinIO",
"success": false,
})
return
}
// List all buckets
buckets, err := minioClient.ListBuckets(context.Background())
if err != nil {
logger.Error(ctx, "Failed to list MinIO buckets", "error", err)
c.JSON(500, gin.H{
"code": 500,
"msg": "Failed to list buckets",
"success": false,
})
return
}
// Get policy for each bucket
bucketInfos := make([]MinioBucketInfo, 0, len(buckets))
for _, bucket := range buckets {
policy := "private" // default
// Try to get bucket policy
policyStr, err := minioClient.GetBucketPolicy(context.Background(), bucket.Name)
if err == nil && policyStr != "" {
// Check if policy contains public read access
if strings.Contains(policyStr, `"Effect":"Allow"`) &&
strings.Contains(policyStr, `"Principal":"*"`) &&
strings.Contains(policyStr, `"s3:GetObject"`) {
policy = "public"
} else {
policy = "custom"
}
}
bucketInfos = append(bucketInfos, MinioBucketInfo{
Name: bucket.Name,
Policy: policy,
CreatedAt: bucket.CreationDate.Format("2006-01-02 15:04:05"),
})
}
logger.Info(ctx, "Listed MinIO buckets successfully", "count", len(bucketInfos))
c.JSON(200, gin.H{
"code": 0,
"msg": "success",
"success": true,
"data": ListMinioBucketsResponse{Buckets: bucketInfos},
})
}
+1
View File
@@ -379,6 +379,7 @@ func RegisterSystemRoutes(r *gin.RouterGroup, handler *handler.SystemHandler) {
systemRoutes := r.Group("/system")
{
systemRoutes.GET("/info", handler.GetSystemInfo)
systemRoutes.GET("/minio/buckets", handler.ListMinioBuckets)
}
}