mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-06-04 13:30:32 +08:00
feat(system): remove MinIO list buckets API and UI dependencies
This commit is contained in:
@@ -57,13 +57,6 @@ type StorageCheckResponse struct {
|
||||
BucketCreated bool `json:"bucket_created,omitempty"`
|
||||
}
|
||||
|
||||
// MinioBucketInfo represents MinIO bucket information
|
||||
type MinioBucketInfo struct {
|
||||
Name string `json:"name"`
|
||||
Policy string `json:"policy"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
}
|
||||
|
||||
// GetSystemInfo gets system version and configuration information
|
||||
func (c *Client) GetSystemInfo(ctx context.Context) (*SystemInfo, error) {
|
||||
resp, err := c.doRequest(ctx, http.MethodGet, "/api/v1/system/info", nil, nil)
|
||||
@@ -154,21 +147,3 @@ func (c *Client) CheckStorageEngine(ctx context.Context, req *StorageCheckReques
|
||||
}
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
// ListMinioBuckets lists all MinIO buckets with their access policies
|
||||
func (c *Client) ListMinioBuckets(ctx context.Context) ([]MinioBucketInfo, error) {
|
||||
resp, err := c.doRequest(ctx, http.MethodGet, "/api/v1/system/minio/buckets", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
Buckets []MinioBucketInfo `json:"buckets"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := parseResponse(resp, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Data.Buckets, nil
|
||||
}
|
||||
|
||||
3046
docs/docs.go
3046
docs/docs.go
File diff suppressed because it is too large
Load Diff
3046
docs/swagger.json
3046
docs/swagger.json
File diff suppressed because it is too large
Load Diff
2335
docs/swagger.yaml
2335
docs/swagger.yaml
File diff suppressed because it is too large
Load Diff
@@ -109,20 +109,6 @@ 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')
|
||||
}
|
||||
|
||||
export interface ParserEngineInfo {
|
||||
Name: string
|
||||
Description: string
|
||||
|
||||
@@ -25,195 +25,201 @@
|
||||
<p class="empty-text">{{ $t('settings.parser.noEngineDetected') }}</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div v-else class="engine-cards">
|
||||
<!-- 当后端未返回 builtin 引擎项时,仍展示 DocReader 状态卡片 -->
|
||||
<div v-if="!hasBuiltinEngine" class="engine-item first" data-model-type="builtin">
|
||||
<div class="engine-item-header">
|
||||
<div class="engine-title-row">
|
||||
<h3>builtin</h3>
|
||||
<t-tag
|
||||
:theme="connected ? 'success' : 'danger'"
|
||||
variant="light"
|
||||
size="small"
|
||||
>{{ connected ? $t('settings.parser.connected') : $t('settings.parser.disconnected') }}</t-tag>
|
||||
</div>
|
||||
<p>{{ $t('settings.parser.builtinDesc') }}</p>
|
||||
</div>
|
||||
<div class="docreader-inline">
|
||||
<div class="status-line">
|
||||
<t-tag
|
||||
:theme="connected ? 'success' : 'danger'"
|
||||
variant="light"
|
||||
size="small"
|
||||
>{{ connected ? $t('settings.parser.connected') : $t('settings.parser.disconnected') }}</t-tag>
|
||||
<t-tag theme="default" variant="light" size="small">{{ docreaderTransport === 'http' ? 'HTTP' : 'gRPC' }}</t-tag>
|
||||
<span v-if="docreaderAddrEnv" class="env-hint">{{ $t('settings.parser.currentAddr') }}: {{ docreaderAddrEnv }}</span>
|
||||
</div>
|
||||
<p class="docreader-desc">
|
||||
{{ $t('settings.parser.envVarHint') }}
|
||||
</p>
|
||||
<div
|
||||
v-if="!hasBuiltinEngine"
|
||||
:class="['engine-card', { active: drawerVisible && currentEngine?.Name === 'builtin' }]"
|
||||
@click="openDrawer({ Name: 'builtin' } as any)"
|
||||
>
|
||||
<div class="engine-card-header">
|
||||
<h3>builtin</h3>
|
||||
<t-tag
|
||||
:theme="connected ? 'success' : 'danger'"
|
||||
variant="light"
|
||||
size="small"
|
||||
>{{ connected ? $t('settings.parser.connected') : $t('settings.parser.disconnected') }}</t-tag>
|
||||
</div>
|
||||
<p class="engine-card-desc">{{ $t('settings.parser.builtinDesc') }}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(engine, idx) in sortedEngines"
|
||||
v-for="engine in sortedEngines"
|
||||
:key="engine.Name"
|
||||
:class="['engine-item', { first: idx === 0 && hasBuiltinEngine }]"
|
||||
:data-model-type="engine.Name"
|
||||
:class="['engine-card', { active: drawerVisible && currentEngine?.Name === engine.Name }]"
|
||||
@click="openDrawer(engine)"
|
||||
>
|
||||
<div class="engine-item-header">
|
||||
<div class="engine-title-row">
|
||||
<h3>{{ getEngineDisplayName(engine.Name) }}</h3>
|
||||
<t-tag v-if="engine.Available" theme="success" variant="light" size="small">{{ $t('settings.parser.available') }}</t-tag>
|
||||
<t-tooltip v-else-if="engine.UnavailableReason" :content="engine.UnavailableReason" placement="top">
|
||||
<t-tag theme="danger" variant="light" size="small" class="tag-with-tooltip">{{ $t('settings.parser.unavailable') }}</t-tag>
|
||||
</t-tooltip>
|
||||
<t-tag v-else theme="danger" variant="light" size="small">{{ $t('settings.parser.unavailable') }}</t-tag>
|
||||
<a
|
||||
v-if="engineDocLink(engine.Name)"
|
||||
:href="engineDocLink(engine.Name)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="engine-doc-link"
|
||||
>{{ engineDocLabel(engine.Name) }} ↗</a>
|
||||
</div>
|
||||
<p>{{ getEngineDisplayDesc(engine.Name, engine.Description) }}</p>
|
||||
</div>
|
||||
|
||||
<!-- builtin: DocReader 连接信息 -->
|
||||
<div v-if="engine.Name === 'builtin'" class="docreader-inline">
|
||||
<div class="status-line">
|
||||
<t-tag v-if="connected" theme="success" variant="light" size="small">{{ $t('settings.parser.connected') }}</t-tag>
|
||||
<t-tag v-else theme="danger" variant="light" size="small">{{ $t('settings.parser.disconnected') }}</t-tag>
|
||||
<t-tag theme="default" variant="light" size="small">{{ docreaderTransport === 'http' ? 'HTTP' : 'gRPC' }}</t-tag>
|
||||
<span v-if="docreaderAddrEnv" class="env-hint">{{ $t('settings.parser.currentAddr') }}: {{ docreaderAddrEnv }}</span>
|
||||
</div>
|
||||
<p class="docreader-desc">
|
||||
{{ $t('settings.parser.envVarHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- weknoracloud: 凭证状态 -->
|
||||
<template v-if="engine.Name === 'weknoracloud'">
|
||||
<div v-if="wkcState === 'configured'" class="wkc-status wkc-status--ok">
|
||||
<t-icon name="check-circle" style="font-size: 15px; color: var(--td-success-color); flex-shrink: 0;" />
|
||||
<span>{{ $t('settings.weknoraCloud.credentialConfigured') }}</span>
|
||||
</div>
|
||||
<div v-else-if="wkcState === 'loading'" class="wkc-status">
|
||||
<t-loading size="small" />
|
||||
<span>{{ $t('settings.weknoraCloud.checkingStatus') }}</span>
|
||||
</div>
|
||||
<div v-else class="wkc-status wkc-status--warn">
|
||||
<t-icon name="error-circle" style="font-size: 15px; color: #f97316; flex-shrink: 0;" />
|
||||
<div style="flex: 1;">
|
||||
<span v-if="wkcState === 'expired'">{{ $t('settings.weknoraCloud.credentialExpired') }}</span>
|
||||
<span v-else>{{ $t('settings.weknoraCloud.unconfigured') }}</span>
|
||||
<div style="margin-top: 6px;">
|
||||
<t-button
|
||||
variant="text"
|
||||
size="small"
|
||||
theme="primary"
|
||||
@click="goToWkcSettings"
|
||||
style="padding: 0; height: auto;"
|
||||
>{{ $t('settings.weknoraCloud.goToSettings') }}</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="engine.FileTypes && engine.FileTypes.length" class="file-types">
|
||||
<t-tag
|
||||
v-for="ft in engine.FileTypes"
|
||||
:key="ft"
|
||||
size="small"
|
||||
variant="light"
|
||||
theme="default"
|
||||
>{{ ft }}</t-tag>
|
||||
</div>
|
||||
|
||||
<!-- mineru 自建配置 -->
|
||||
<div v-if="engine.Name === 'mineru'" class="engine-form">
|
||||
<div class="form-field">
|
||||
<label>{{ t('settings.parser.selfHostedEndpoint') }}</label>
|
||||
<t-input
|
||||
v-model="config.mineru_endpoint"
|
||||
:placeholder="$t('settings.parser.mineruEndpointPlaceholder')"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label>Backend</label>
|
||||
<t-select v-model="config.mineru_model" :placeholder="$t('settings.parser.defaultPipeline')" clearable>
|
||||
<t-option value="pipeline" label="pipeline" />
|
||||
<t-option value="vlm-auto-engine" label="vlm-auto-engine" />
|
||||
<t-option value="vlm-http-client" label="vlm-http-client" />
|
||||
<t-option value="hybrid-auto-engine" label="hybrid-auto-engine" />
|
||||
<t-option value="hybrid-http-client" label="hybrid-http-client" />
|
||||
</t-select>
|
||||
</div>
|
||||
<div class="form-toggles">
|
||||
<t-checkbox v-model="config.mineru_enable_formula">{{ $t('settings.parser.formulaRecognition') }}</t-checkbox>
|
||||
<t-checkbox v-model="config.mineru_enable_table">{{ $t('settings.parser.tableRecognition') }}</t-checkbox>
|
||||
<t-checkbox v-model="config.mineru_enable_ocr">OCR</t-checkbox>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label>{{ t('settings.parser.language') }}</label>
|
||||
<t-input
|
||||
v-model="config.mineru_language"
|
||||
:placeholder="$t('settings.parser.languagePlaceholder')"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- mineru_cloud 云 API 配置 -->
|
||||
<div v-if="engine.Name === 'mineru_cloud'" class="engine-form">
|
||||
<div class="form-field">
|
||||
<label>API Key</label>
|
||||
<t-input
|
||||
v-model="config.mineru_api_key"
|
||||
type="password"
|
||||
:placeholder="$t('settings.parser.mineruCloudApiKeyPlaceholder')"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label>Model Version</label>
|
||||
<t-select v-model="config.mineru_cloud_model" :placeholder="$t('settings.parser.defaultPipeline')" clearable>
|
||||
<t-option value="pipeline" label="pipeline" />
|
||||
<t-option value="vlm" :label="$t('settings.parser.vlmLabel')" />
|
||||
<t-option value="MinerU-HTML" :label="$t('settings.parser.mineruHtmlLabel')" />
|
||||
</t-select>
|
||||
</div>
|
||||
<div class="form-toggles">
|
||||
<t-checkbox v-model="config.mineru_cloud_enable_formula">{{ $t('settings.parser.formulaRecognition') }}</t-checkbox>
|
||||
<t-checkbox v-model="config.mineru_cloud_enable_table">{{ $t('settings.parser.tableRecognition') }}</t-checkbox>
|
||||
<t-checkbox v-model="config.mineru_cloud_enable_ocr">OCR</t-checkbox>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label>{{ t('settings.parser.language') }}</label>
|
||||
<t-input
|
||||
v-model="config.mineru_cloud_language"
|
||||
:placeholder="$t('settings.parser.languagePlaceholder')"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="engine-card-header">
|
||||
<h3>{{ getEngineDisplayName(engine.Name) }}</h3>
|
||||
<t-tag v-if="engine.Available" theme="success" variant="light" size="small">{{ $t('settings.parser.available') }}</t-tag>
|
||||
<t-tooltip v-else-if="engine.UnavailableReason" :content="engine.UnavailableReason" placement="top">
|
||||
<t-tag theme="danger" variant="light" size="small" class="tag-with-tooltip">{{ $t('settings.parser.unavailable') }}</t-tag>
|
||||
</t-tooltip>
|
||||
<t-tag v-else theme="danger" variant="light" size="small">{{ $t('settings.parser.unavailable') }}</t-tag>
|
||||
</div>
|
||||
<p class="engine-card-desc">{{ getEngineDisplayDesc(engine.Name, engine.Description) }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 检测与保存 -->
|
||||
<div class="save-bar">
|
||||
<t-button theme="default" variant="outline" :loading="checking" @click="onCheck">
|
||||
{{ $t('settings.parser.checkWithParams') }}
|
||||
</t-button>
|
||||
<t-button theme="primary" :loading="saving" @click="onSave">{{ $t('settings.parser.saveConfig') }}</t-button>
|
||||
<span v-if="checkMessage" class="save-msg hint">{{ checkMessage }}</span>
|
||||
<span v-else-if="saveMessage" :class="['save-msg', saveSuccess ? 'success' : 'error']">
|
||||
{{ saveMessage }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 配置抽屉 -->
|
||||
<t-drawer
|
||||
v-model:visible="drawerVisible"
|
||||
:header="drawerTitle"
|
||||
size="500px"
|
||||
>
|
||||
<div v-if="currentEngine" class="drawer-content">
|
||||
<div class="engine-info-block">
|
||||
<p class="engine-desc">{{ getEngineDisplayDesc(currentEngine.Name, currentEngine.Description) }}
|
||||
<a
|
||||
v-if="engineDocLink(currentEngine.Name)"
|
||||
:href="engineDocLink(currentEngine.Name)"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="engine-doc-link"
|
||||
>{{ engineDocLabel(currentEngine.Name) }} ↗</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- builtin: DocReader 连接信息 -->
|
||||
<div v-if="currentEngine.Name === 'builtin'" class="docreader-inline">
|
||||
<div class="status-line">
|
||||
<t-tag v-if="connected" theme="success" variant="light" size="small">{{ $t('settings.parser.connected') }}</t-tag>
|
||||
<t-tag v-else theme="danger" variant="light" size="small">{{ $t('settings.parser.disconnected') }}</t-tag>
|
||||
<t-tag theme="default" variant="light" size="small">{{ docreaderTransport === 'http' ? 'HTTP' : 'gRPC' }}</t-tag>
|
||||
<span v-if="docreaderAddrEnv" class="env-hint">{{ $t('settings.parser.currentAddr') }}: {{ docreaderAddrEnv }}</span>
|
||||
</div>
|
||||
<p class="docreader-desc">
|
||||
{{ $t('settings.parser.envVarHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- weknoracloud: 凭证状态 -->
|
||||
<template v-if="currentEngine.Name === 'weknoracloud'">
|
||||
<div v-if="wkcState === 'configured'" class="wkc-status wkc-status--ok">
|
||||
<t-icon name="check-circle" style="font-size: 15px; color: var(--td-success-color); flex-shrink: 0;" />
|
||||
<span>{{ $t('settings.weknoraCloud.credentialConfigured') }}</span>
|
||||
</div>
|
||||
<div v-else-if="wkcState === 'loading'" class="wkc-status">
|
||||
<t-loading size="small" />
|
||||
<span>{{ $t('settings.weknoraCloud.checkingStatus') }}</span>
|
||||
</div>
|
||||
<div v-else class="wkc-status wkc-status--warn">
|
||||
<t-icon name="error-circle" style="font-size: 15px; color: #f97316; flex-shrink: 0;" />
|
||||
<div style="flex: 1;">
|
||||
<span v-if="wkcState === 'expired'">{{ $t('settings.weknoraCloud.credentialExpired') }}</span>
|
||||
<span v-else>{{ $t('settings.weknoraCloud.unconfigured') }}</span>
|
||||
<div style="margin-top: 6px;">
|
||||
<t-button
|
||||
variant="text"
|
||||
size="small"
|
||||
theme="primary"
|
||||
@click="goToWkcSettings"
|
||||
style="padding: 0; height: auto;"
|
||||
>{{ $t('settings.weknoraCloud.goToSettings') }}</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="currentEngine.FileTypes && currentEngine.FileTypes.length" class="file-types">
|
||||
<t-tag
|
||||
v-for="ft in currentEngine.FileTypes"
|
||||
:key="ft"
|
||||
size="small"
|
||||
variant="light"
|
||||
theme="default"
|
||||
>{{ ft }}</t-tag>
|
||||
</div>
|
||||
|
||||
<!-- mineru 自建配置 -->
|
||||
<div v-if="currentEngine.Name === 'mineru'" class="engine-form">
|
||||
<div class="form-item">
|
||||
<label class="form-label">{{ t('settings.parser.selfHostedEndpoint') }}</label>
|
||||
<t-input
|
||||
v-model="config.mineru_endpoint"
|
||||
:placeholder="$t('settings.parser.mineruEndpointPlaceholder')"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label class="form-label">Backend</label>
|
||||
<t-select v-model="config.mineru_model" :placeholder="$t('settings.parser.defaultPipeline')" clearable>
|
||||
<t-option value="pipeline" label="pipeline" />
|
||||
<t-option value="vlm-auto-engine" label="vlm-auto-engine" />
|
||||
<t-option value="vlm-http-client" label="vlm-http-client" />
|
||||
<t-option value="hybrid-auto-engine" label="hybrid-auto-engine" />
|
||||
<t-option value="hybrid-http-client" label="hybrid-http-client" />
|
||||
</t-select>
|
||||
</div>
|
||||
<div class="form-toggles">
|
||||
<t-checkbox v-model="config.mineru_enable_formula">{{ $t('settings.parser.formulaRecognition') }}</t-checkbox>
|
||||
<t-checkbox v-model="config.mineru_enable_table">{{ $t('settings.parser.tableRecognition') }}</t-checkbox>
|
||||
<t-checkbox v-model="config.mineru_enable_ocr">OCR</t-checkbox>
|
||||
</div>
|
||||
<div class="form-item" style="margin-top: 16px;">
|
||||
<label class="form-label">{{ t('settings.parser.language') }}</label>
|
||||
<t-input
|
||||
v-model="config.mineru_language"
|
||||
:placeholder="$t('settings.parser.languagePlaceholder')"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- mineru_cloud 云 API 配置 -->
|
||||
<div v-if="currentEngine.Name === 'mineru_cloud'" class="engine-form">
|
||||
<div class="form-item">
|
||||
<label class="form-label">API Key</label>
|
||||
<t-input
|
||||
v-model="config.mineru_api_key"
|
||||
type="password"
|
||||
:placeholder="$t('settings.parser.mineruCloudApiKeyPlaceholder')"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label class="form-label">Model Version</label>
|
||||
<t-select v-model="config.mineru_cloud_model" :placeholder="$t('settings.parser.defaultPipeline')" clearable>
|
||||
<t-option value="pipeline" label="pipeline" />
|
||||
<t-option value="vlm" :label="$t('settings.parser.vlmLabel')" />
|
||||
<t-option value="MinerU-HTML" :label="$t('settings.parser.mineruHtmlLabel')" />
|
||||
</t-select>
|
||||
</div>
|
||||
<div class="form-toggles">
|
||||
<t-checkbox v-model="config.mineru_cloud_enable_formula">{{ $t('settings.parser.formulaRecognition') }}</t-checkbox>
|
||||
<t-checkbox v-model="config.mineru_cloud_enable_table">{{ $t('settings.parser.tableRecognition') }}</t-checkbox>
|
||||
<t-checkbox v-model="config.mineru_cloud_enable_ocr">OCR</t-checkbox>
|
||||
</div>
|
||||
<div class="form-item" style="margin-top: 16px;">
|
||||
<label class="form-label">{{ t('settings.parser.language') }}</label>
|
||||
<t-input
|
||||
v-model="config.mineru_cloud_language"
|
||||
:placeholder="$t('settings.parser.languagePlaceholder')"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item" v-if="currentEngine && (hasConfigFields(currentEngine.Name) || currentEngine.Name === 'builtin')">
|
||||
<label class="form-label">{{ $t('settings.parser.testConnection', '测试连接') }}</label>
|
||||
<div class="api-test-section">
|
||||
<t-button variant="outline" :loading="checking" @click="onCheck">
|
||||
{{ $t('settings.parser.testConnection', '测试连接') }}
|
||||
</t-button>
|
||||
<span v-if="checkMessage || saveMessage" :class="['test-message', saveSuccess && !checkMessage ? 'success' : (checkMessage ? 'hint' : 'error')]">
|
||||
{{ checkMessage || saveMessage }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="drawer-footer-actions">
|
||||
<t-button theme="default" variant="outline" @click="drawerVisible = false">{{ $t('common.cancel') }}</t-button>
|
||||
<t-button theme="primary" :loading="saving" @click="onSave">{{ $t('common.save') }}</t-button>
|
||||
</div>
|
||||
</template>
|
||||
</t-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -278,6 +284,12 @@ const checkMessage = ref('')
|
||||
|
||||
const hasBuiltinEngine = computed(() => engines.value.some(e => e.Name === 'builtin'))
|
||||
|
||||
const drawerVisible = ref(false)
|
||||
const currentEngine = ref<ParserEngineInfo | null>(null)
|
||||
const drawerTitle = computed(() => {
|
||||
return currentEngine.value ? getEngineDisplayName(currentEngine.value.Name) : ''
|
||||
})
|
||||
|
||||
/** 固定展示顺序,未列出的引擎排在末尾按名称排序 */
|
||||
const ENGINE_ORDER: Record<string, number> = {
|
||||
builtin: 0,
|
||||
@@ -321,6 +333,13 @@ function getEngineDisplayDesc(engineName: string, fallback: string): string {
|
||||
return translated !== key ? translated : fallback
|
||||
}
|
||||
|
||||
function openDrawer(engine: ParserEngineInfo) {
|
||||
currentEngine.value = engine
|
||||
drawerVisible.value = true
|
||||
saveMessage.value = ''
|
||||
checkMessage.value = ''
|
||||
}
|
||||
|
||||
async function loadEngines() {
|
||||
try {
|
||||
const res = await getParserEngines()
|
||||
@@ -394,13 +413,47 @@ async function onCheck() {
|
||||
}
|
||||
checking.value = true
|
||||
checkMessage.value = ''
|
||||
saveMessage.value = ''
|
||||
try {
|
||||
const res = await checkParserEngines(buildConfigPayload())
|
||||
engines.value = res?.data ?? []
|
||||
checkMessage.value = t('settings.parser.checkDoneStatusUpdated')
|
||||
if (res?.connected !== undefined) {
|
||||
connected.value = res.connected
|
||||
}
|
||||
|
||||
if (currentEngine.value) {
|
||||
if (currentEngine.value.Name === 'builtin') {
|
||||
if (connected.value) {
|
||||
checkMessage.value = t('settings.parser.checkSuccess', '测试连接成功')
|
||||
saveSuccess.value = true
|
||||
} else {
|
||||
checkMessage.value = t('settings.parser.checkFailed', '测试连接失败')
|
||||
saveSuccess.value = false
|
||||
}
|
||||
} else {
|
||||
const updatedEngine = engines.value.find(e => e.Name === currentEngine.value!.Name)
|
||||
if (updatedEngine) {
|
||||
if (updatedEngine.Available) {
|
||||
checkMessage.value = t('settings.parser.checkSuccess', '测试连接成功')
|
||||
saveSuccess.value = true
|
||||
} else {
|
||||
checkMessage.value = updatedEngine.UnavailableReason || t('settings.parser.checkFailed', '测试连接失败')
|
||||
saveSuccess.value = false
|
||||
}
|
||||
} else {
|
||||
checkMessage.value = t('settings.parser.checkFailed', '引擎状态未知')
|
||||
saveSuccess.value = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
checkMessage.value = t('settings.parser.checkDoneStatusUpdated', '检测已完成,状态已更新')
|
||||
saveSuccess.value = true
|
||||
}
|
||||
|
||||
setTimeout(() => { checkMessage.value = '' }, 3000)
|
||||
} catch (e: any) {
|
||||
checkMessage.value = e?.message || t('settings.parser.checkFailed')
|
||||
checkMessage.value = e?.message || t('settings.parser.checkFailed', '测试连接失败')
|
||||
saveSuccess.value = false
|
||||
} finally {
|
||||
checking.value = false
|
||||
}
|
||||
@@ -413,6 +466,7 @@ async function onSave() {
|
||||
await updateParserEngineConfig(buildConfigPayload())
|
||||
saveSuccess.value = true
|
||||
saveMessage.value = t('settings.parser.saveSuccess')
|
||||
drawerVisible.value = false
|
||||
loadEngines()
|
||||
} catch (e: any) {
|
||||
saveSuccess.value = false
|
||||
@@ -500,34 +554,41 @@ onMounted(loadAll)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 引擎条目 ----
|
||||
.engine-item {
|
||||
padding-top: 24px;
|
||||
// ---- 引擎卡片布局 ----
|
||||
.engine-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
border-top: 1px solid var(--td-component-stroke);
|
||||
}
|
||||
|
||||
&.first {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
.engine-card {
|
||||
border: 1px solid var(--td-component-stroke);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: var(--td-bg-color-container);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--td-brand-color);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--td-brand-color);
|
||||
background: rgba(var(--td-brand-color-5-rgba), 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.engine-item-header {
|
||||
margin-bottom: 16px;
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
color: var(--td-text-color-placeholder);
|
||||
margin: 6px 0 0 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.engine-title-row {
|
||||
.engine-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
@@ -535,30 +596,86 @@ onMounted(loadAll)
|
||||
color: var(--td-text-color-primary);
|
||||
margin: 0;
|
||||
font-family: 'SF Mono', 'Monaco', 'Menlo', monospace;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.engine-card-desc {
|
||||
font-size: 13px;
|
||||
color: var(--td-text-color-secondary);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// ---- 抽屉内容 ----
|
||||
.drawer-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.engine-info-block {
|
||||
.engine-desc {
|
||||
font-size: 13px;
|
||||
color: var(--td-text-color-secondary);
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框样式
|
||||
:deep(.t-input),
|
||||
:deep(.t-select) {
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
|
||||
.t-input__inner,
|
||||
.t-input__wrap,
|
||||
input {
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
border-color: var(--td-component-stroke);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
&:hover .t-input__inner,
|
||||
&:hover .t-input__wrap,
|
||||
&:hover input {
|
||||
border-color: var(--td-component-stroke);
|
||||
}
|
||||
|
||||
&.t-is-focused .t-input__inner,
|
||||
&.t-is-focused .t-input__wrap,
|
||||
&.t-is-focused input {
|
||||
border-color: var(--td-brand-color);
|
||||
box-shadow: 0 0 0 2px rgba(7, 192, 95, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.engine-doc-link {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--td-brand-color);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- DocReader 连接信息 ----
|
||||
.docreader-inline {
|
||||
padding: 10px 14px;
|
||||
padding: 12px 16px;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.status-line {
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,13 +684,6 @@ onMounted(loadAll)
|
||||
font-size: 12px;
|
||||
color: var(--td-text-color-placeholder);
|
||||
line-height: 1.6;
|
||||
|
||||
code {
|
||||
padding: 1px 5px;
|
||||
font-size: 11px;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.status-line {
|
||||
@@ -593,28 +703,35 @@ onMounted(loadAll)
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
// ---- 配置表单 ----
|
||||
.engine-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px dashed var(--td-component-stroke);
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--td-text-color-secondary);
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--td-text-color-primary);
|
||||
|
||||
&.required::after {
|
||||
content: '*';
|
||||
color: var(--td-error-color);
|
||||
margin-left: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,35 +739,7 @@ onMounted(loadAll)
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
// ---- 保存栏(sticky) ----
|
||||
.save-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
margin-top: 32px;
|
||||
padding: 16px 0 4px;
|
||||
background: linear-gradient(to bottom, transparent 0%, var(--td-bg-color-container) 12%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.save-msg {
|
||||
font-size: 13px;
|
||||
|
||||
&.success {
|
||||
color: var(--td-success-color);
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: var(--td-error-color);
|
||||
}
|
||||
|
||||
&.hint {
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tag-with-tooltip {
|
||||
@@ -662,11 +751,10 @@ onMounted(loadAll)
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--td-text-color-secondary);
|
||||
margin-bottom: 12px;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
|
||||
&--ok {
|
||||
@@ -681,4 +769,76 @@ onMounted(loadAll)
|
||||
border-left: 3px solid #f97316;
|
||||
}
|
||||
}
|
||||
|
||||
.api-test-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.test-message {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
flex: 1;
|
||||
|
||||
&.success {
|
||||
color: var(--td-brand-color-active);
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: var(--td-error-color);
|
||||
}
|
||||
|
||||
&.hint {
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.t-button) {
|
||||
min-width: 88px;
|
||||
height: 32px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.available {
|
||||
color: var(--td-brand-color);
|
||||
}
|
||||
|
||||
&.unavailable {
|
||||
color: var(--td-error-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-footer-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
|
||||
:deep(.t-button) {
|
||||
min-width: 80px;
|
||||
height: 36px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&.t-button--variant-outline {
|
||||
color: var(--td-text-color-secondary);
|
||||
border-color: var(--td-component-stroke);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--td-brand-color);
|
||||
color: var(--td-brand-color);
|
||||
background: rgba(7, 192, 95, 0.04);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -186,45 +186,11 @@ const navItems = computed(() => [
|
||||
{ key: 'general', icon: 'setting', label: t('general.title') },
|
||||
{ key: 'ollama', icon: 'server', label: 'Ollama' },
|
||||
{ key: 'weknoracloud', icon: '', label: 'WeKnora Cloud' },
|
||||
{
|
||||
key: 'models',
|
||||
icon: 'control-platform',
|
||||
label: t('settings.modelManagement'),
|
||||
children: [
|
||||
{ key: 'chat', label: t('model.llmModel') },
|
||||
{ key: 'embedding', label: t('model.embeddingModel') },
|
||||
{ key: 'rerank', label: t('model.rerankModel') },
|
||||
{ key: 'vllm', label: t('model.vlmModel') }
|
||||
]
|
||||
},
|
||||
{ key: 'models', icon: 'control-platform', label: t('settings.modelManagement') },
|
||||
{ key: 'websearch', icon: 'search', label: t('settings.webSearchConfig') },
|
||||
{ key: 'chathistory', icon: 'chat', label: t('chatHistorySettings.title') },
|
||||
{
|
||||
key: 'parser',
|
||||
icon: 'file-search',
|
||||
label: t('settings.parserEngine'),
|
||||
children: [
|
||||
{ key: 'builtin', label: 'Builtin (DocReader)' },
|
||||
{ key: 'weknoracloud', label: 'WeKnora Cloud' },
|
||||
{ key: 'simple', label: 'Simple' },
|
||||
{ key: 'markitdown', label: 'Markitdown' },
|
||||
{ key: 'mineru', label: 'MinerU' },
|
||||
{ key: 'mineru_cloud', label: 'MinerU Cloud' },
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'storage',
|
||||
icon: 'cloud',
|
||||
label: t('settings.storageEngine'),
|
||||
children: [
|
||||
{ key: 'local', label: 'Local' },
|
||||
{ key: 'minio', label: 'MinIO' },
|
||||
{ key: 'cos', label: t('settings.storage.cos') },
|
||||
{ key: 'tos', label: t('settings.storage.tos') },
|
||||
{ key: 's3', label: 'AWS S3' },
|
||||
{ key: 'oss', label: t('settings.storage.oss') },
|
||||
]
|
||||
},
|
||||
{ key: 'parser', icon: 'file-search', label: t('settings.parserEngine') },
|
||||
{ key: 'storage', icon: 'cloud', label: t('settings.storageEngine') },
|
||||
{ key: 'mcp', icon: 'tools', label: t('settings.mcpService') },
|
||||
{ key: 'system', icon: 'info-circle', label: t('settings.systemSettings') },
|
||||
{ key: 'tenant', icon: 'user-circle', label: t('settings.tenantInfo') },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,6 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
@@ -442,18 +441,6 @@ func (h *SystemHandler) isTOSEnvAvailable() bool {
|
||||
os.Getenv("TOS_BUCKET_NAME") != ""
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// StorageEngineStatusItem describes one storage engine's availability and description.
|
||||
type StorageEngineStatusItem struct {
|
||||
Name string `json:"name"` // "local", "minio", "cos", "tos"
|
||||
@@ -494,188 +481,7 @@ func (h *SystemHandler) GetStorageEngineStatus(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// 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())
|
||||
|
||||
endpoint, accessKeyID, secretAccessKey := h.getMinioConfig(c)
|
||||
if endpoint == "" || accessKeyID == "" || secretAccessKey == "" {
|
||||
logger.Warn(ctx, "MinIO is not configured")
|
||||
c.JSON(400, gin.H{
|
||||
"code": 400,
|
||||
"msg": "MinIO is not configured",
|
||||
"success": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
useSSL := os.Getenv("MINIO_USE_SSL") == "true"
|
||||
if v, exists := c.Get(types.TenantInfoContextKey.String()); exists {
|
||||
if tenant, ok := v.(*types.Tenant); ok && tenant != nil && tenant.StorageEngineConfig != nil && tenant.StorageEngineConfig.MinIO != nil {
|
||||
useSSL = tenant.StorageEngineConfig.MinIO.UseSSL
|
||||
}
|
||||
}
|
||||
|
||||
// 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: no policy means private
|
||||
|
||||
// Try to get bucket policy
|
||||
policyStr, err := minioClient.GetBucketPolicy(context.Background(), bucket.Name)
|
||||
if err == nil && policyStr != "" {
|
||||
policy = parseBucketPolicy(policyStr)
|
||||
}
|
||||
// If err != nil or policyStr is empty, bucket has no policy (private)
|
||||
|
||||
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},
|
||||
})
|
||||
}
|
||||
|
||||
// BucketPolicy represents the S3 bucket policy structure
|
||||
type BucketPolicy struct {
|
||||
Version string `json:"Version"`
|
||||
Statement []PolicyStatement `json:"Statement"`
|
||||
}
|
||||
|
||||
// PolicyStatement represents a single statement in the bucket policy
|
||||
type PolicyStatement struct {
|
||||
Effect string `json:"Effect"`
|
||||
Principal interface{} `json:"Principal"` // Can be "*" or {"AWS": [...]}
|
||||
Action interface{} `json:"Action"` // Can be string or []string
|
||||
Resource interface{} `json:"Resource"` // Can be string or []string
|
||||
}
|
||||
|
||||
// parseBucketPolicy parses the policy JSON and determines the access type
|
||||
func parseBucketPolicy(policyStr string) string {
|
||||
var policy BucketPolicy
|
||||
if err := json.Unmarshal([]byte(policyStr), &policy); err != nil {
|
||||
// If we can't parse the policy, treat it as custom
|
||||
return "custom"
|
||||
}
|
||||
|
||||
// Check if any statement grants public read access
|
||||
hasPublicRead := false
|
||||
for _, stmt := range policy.Statement {
|
||||
if stmt.Effect != "Allow" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if Principal is "*" (public)
|
||||
if !isPrincipalPublic(stmt.Principal) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if Action includes s3:GetObject
|
||||
if !hasGetObjectAction(stmt.Action) {
|
||||
continue
|
||||
}
|
||||
|
||||
hasPublicRead = true
|
||||
break
|
||||
}
|
||||
|
||||
if hasPublicRead {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// Has policy but not public read
|
||||
return "custom"
|
||||
}
|
||||
|
||||
// isPrincipalPublic checks if the principal allows public access
|
||||
func isPrincipalPublic(principal interface{}) bool {
|
||||
switch p := principal.(type) {
|
||||
case string:
|
||||
return p == "*"
|
||||
case map[string]interface{}:
|
||||
// Check for {"AWS": "*"} or {"AWS": ["*"]}
|
||||
if aws, ok := p["AWS"]; ok {
|
||||
switch a := aws.(type) {
|
||||
case string:
|
||||
return a == "*"
|
||||
case []interface{}:
|
||||
for _, v := range a {
|
||||
if s, ok := v.(string); ok && s == "*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasGetObjectAction checks if the action includes s3:GetObject
|
||||
func hasGetObjectAction(action interface{}) bool {
|
||||
checkAction := func(a string) bool {
|
||||
a = strings.ToLower(a)
|
||||
return a == "s3:getobject" || a == "s3:*" || a == "*"
|
||||
}
|
||||
|
||||
switch act := action.(type) {
|
||||
case string:
|
||||
return checkAction(act)
|
||||
case []interface{}:
|
||||
for _, v := range act {
|
||||
if s, ok := v.(string); ok && checkAction(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// --- Storage engine helpers ---
|
||||
|
||||
// cosFieldPattern validates COS region and bucket name format to prevent URL injection.
|
||||
var cosFieldPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$`)
|
||||
|
||||
|
||||
@@ -448,7 +448,6 @@ func RegisterSystemRoutes(r *gin.RouterGroup, handler *handler.SystemHandler) {
|
||||
systemRoutes.POST("/docreader/reconnect", handler.ReconnectDocReader)
|
||||
systemRoutes.GET("/storage-engine-status", handler.GetStorageEngineStatus)
|
||||
systemRoutes.POST("/storage-engine-check", handler.CheckStorageEngine)
|
||||
systemRoutes.GET("/minio/buckets", handler.ListMinioBuckets)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user