Files
Enterprise-Knowledge-Base/frontend/src/views/settings/StorageEngineSettings.vue
T
wizardchen d38a6efd87 refactor(agent): remove final_answer tool references and update related logic
- Eliminated all instances of the final_answer tool from the agent's configuration and logic, transitioning to a model where the agent concludes responses with plain text instead.
- Updated event handling and response analysis to reflect this change, ensuring that the agent's output is now directly delivered as text without relying on a dedicated final answer tool.
- Adjusted localization strings across multiple languages to remove references to final_answer, enhancing clarity and consistency in user-facing messages.
- Improved the overall structure of the agent's response handling to streamline the process and reduce potential confusion regarding tool usage.

refactor(agent): streamline answer handling and preamble logic

- Updated the agent's response handling to treat any text streamed before a tool call as a preamble, ensuring it is not included in the final answer.
- Removed the superseded marker logic from the event emission process, simplifying the flow of answer content and enhancing clarity in the UI.
- Adjusted the event handling to ensure that preamble segments are correctly retracted and relocated into the steps tree, improving the overall user experience.
- Enhanced the agent's internal logic to better manage answer segments and their states during streaming, leading to a more coherent response structure.
2026-06-04 15:48:04 +08:00

1688 lines
59 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="storage-engine-settings">
<div class="section-header">
<h2>{{ $t('settings.storage.title') }}</h2>
<p class="section-description">
{{ $t('settings.storage.description') }}
</p>
</div>
<div v-if="loading" class="loading-state">
<t-loading size="small" />
<span>{{ $t('settings.storage.loading') }}</span>
</div>
<div v-else-if="error" class="error-inline">
<t-alert theme="error" :message="error">
<template #operation>
<t-button size="small" @click="loadAll">{{ $t('settings.storage.retry') }}</t-button>
</template>
</t-alert>
</div>
<template v-else>
<div class="settings-group">
<div class="setting-row">
<div class="setting-info">
<label>{{ $t('settings.storage.defaultEngine') }}</label>
<p class="desc">{{ $t('settings.storage.defaultEngineDesc') }}</p>
</div>
<div class="setting-control">
<t-select
v-model="config.default_provider"
style="width: 280px;"
:placeholder="$t('settings.storage.defaultEngine')"
:disabled="!hasAllowedProviders"
@change="onSaveDefaultEngine"
>
<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 }}
</span>
</div>
</div>
</div>
<!-- 与其它 settings 列表同形:左侧 monogram 徽章 + 标题 + 状态徽 + 描述。
整张卡是一个 button,单击打开配置抽屉;当前抽屉对应的卡获得品牌色描边。
原本 8 张手写卡片由统一的 STORAGE_PROVIDERS 数组驱动,把状态判定收敛到
providerStatus(),新增 provider 时只需在数组里加一项 + 翻译键即可。 -->
<div class="engine-cards">
<button
v-for="provider in STORAGE_PROVIDERS"
v-show="isProviderAllowed(provider.id)"
:key="provider.id"
type="button"
class="engine-card"
:class="[
`engine-card--${provider.id}`,
{ 'engine-card--active': drawerVisible && currentEngine === provider.id }
]"
@click="openDrawer(provider.id)"
>
<div
class="engine-card__badge"
:class="badgeClass(provider.id)"
:style="badgeStyle(provider.id)"
:aria-label="provider.id"
>
<img
v-if="resolveLogo(provider.id)?.mode === 'color'"
:src="resolveLogo(provider.id)!.url"
:alt="provider.id"
class="engine-card__badge-img"
/>
<template v-else-if="!resolveLogo(provider.id)">{{ providerInitial(provider.id) }}</template>
</div>
<div class="engine-card__body">
<div class="engine-card__header">
<h3 class="engine-card__title">{{ providerTitle(provider.id) }}</h3>
<span
class="engine-card__status"
:class="`engine-card__status--${providerStatus(provider.id).kind}`"
>
<span class="engine-card__status-dot" />
{{ providerStatus(provider.id).label }}
</span>
</div>
<p class="engine-card__desc">{{ $t(`settings.storage.${provider.id}Desc`) }}</p>
</div>
</button>
</div>
</template>
<!-- 配置抽屉 — SettingDrawer 包装,与 ModelEditorDialog / ParserEngineSettings 同款 -->
<SettingDrawer
v-model:visible="drawerVisible"
:title="drawerTitle"
:class="currentEngine ? `storage-engine-drawer storage-engine-drawer--${currentEngine}` : 'storage-engine-drawer'"
:hide-footer="!authStore.hasRole('admin') && !needsTestButton"
:confirm-loading="saving"
@confirm="onSave"
@cancel="drawerVisible = false"
>
<!--
Header icon — 复用列表卡片同款 logo / 配色徽章。
- color logo(如 MinIO/AWS): 直接 <img>,保留品牌彩色
- mono logo(mask-image): 通过 ::before + currentColor 染色,颜色由
.storage-engine-drawer--{id} :deep(.setting-drawer__header-icon) 决定
- 无 logo(local): 渲染首字母作为 monogram
-->
<template v-if="currentEngine" #headerIcon>
<img
v-if="currentLogo?.mode === 'color'"
:src="currentLogo.url"
:alt="currentEngine"
class="header-icon__img"
/>
<span
v-else-if="currentLogo?.mode === 'mono'"
class="header-icon__mono"
:style="monoLogoStyle"
/>
<span v-else class="header-icon__text">{{ providerInitial(currentEngine as StorageProviderId) }}</span>
</template>
<!-- 副标题:引擎描述 + inline 控制台/文档外链(若有) -->
<template v-if="currentEngine" #subtitle>
<span>{{ engineDescText }}</span>
<template v-for="link in engineLinks" :key="link.url">
<a :href="link.url" target="_blank" rel="noopener" class="doc-link doc-link--inline">
{{ link.label }}
<t-icon name="link" class="link-icon" />
</a>
</template>
</template>
<!-- 测试连接挪到 footer-left(local 不需要) -->
<template v-if="needsTestButton" #footer-left>
<t-button
variant="outline"
:loading="currentCheckState.loading"
@click="currentCheckState.onCheck"
>
<template #icon>
<t-icon
v-if="!currentCheckState.loading && currentCheckState.result?.ok"
name="check-circle-filled"
class="status-icon available"
/>
<t-icon
v-else-if="!currentCheckState.loading && currentCheckState.result && !currentCheckState.result.ok"
name="close-circle-filled"
class="status-icon unavailable"
/>
</template>
{{ $t('settings.storage.testConnection') }}
</t-button>
<span
v-if="currentCheckState.result"
:class="[
'footer-test-message',
currentCheckState.result.ok
? (currentCheckState.result.bucket_created ? 'created' : 'success')
: 'error'
]"
:title="currentCheckState.result.message"
>
{{ currentCheckState.result.message }}
</span>
</template>
<div v-if="currentEngine">
<!-- ===== local ===== -->
<template v-if="currentEngine === 'local'">
<section class="setting-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('settings.storage.basicSection', '基本配置') }}</h4>
<div class="form-item">
<label class="form-label">{{ $t('settings.storage.pathPrefix') }}</label>
<t-input
v-model="config.local.path_prefix"
:placeholder="$t('settings.storage.pathPrefixPlaceholder')"
clearable
/>
</div>
</section>
</template>
<!-- ===== minio ===== -->
<template v-else-if="currentEngine === 'minio'">
<!-- Section 1 — 部署模式(Docker / 远程) -->
<section class="setting-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('settings.storage.modeSection', '部署模式') }}</h4>
<div class="form-item">
<div class="source-options" role="radiogroup">
<button
type="button"
class="source-option"
:class="{ 'is-active': config.minio.mode !== 'remote' }"
@click="config.minio.mode = 'docker'"
>
<t-icon name="server" class="source-option__icon" />
<span class="source-option__label">{{ $t('settings.storage.minioDocker') }}</span>
</button>
<button
type="button"
class="source-option"
:class="{ 'is-active': config.minio.mode === 'remote' }"
@click="config.minio.mode = 'remote'"
>
<t-icon name="cloud" class="source-option__icon" />
<span class="source-option__label">{{ $t('settings.storage.minioRemote') }}</span>
</button>
</div>
<!-- Docker 模式状态提示 inline-alert -->
<div v-if="config.minio.mode !== 'remote'" class="inline-alert"
:class="minioEnvAvailable ? 'inline-alert--ok' : 'inline-alert--warn'">
<t-icon
:name="minioEnvAvailable ? 'check-circle-filled' : 'error-circle-filled'"
class="inline-alert__icon"
/>
<span class="inline-alert__text">
{{ minioEnvAvailable ? $t('settings.storage.minioDockerDetected') : $t('settings.storage.minioDockerNotDetected') }}
</span>
</div>
<div v-else class="inline-alert">
<t-icon name="info-circle-filled" class="inline-alert__icon" />
<span class="inline-alert__text">{{ $t('settings.storage.minioRemoteHint') }}</span>
</div>
</div>
</section>
<!-- Section 2 — 远程模式凭证(仅 remote) -->
<section v-if="config.minio.mode === 'remote'" class="setting-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('settings.storage.credentialsSection', '凭证') }}</h4>
<div class="form-item">
<label class="form-label required">Endpoint</label>
<t-input v-model="config.minio.endpoint" placeholder="e.g. minio.example.com:9000" clearable />
</div>
<div class="form-item">
<label class="form-label required">Access Key ID</label>
<t-input v-model="config.minio.access_key_id" placeholder="MinIO Access Key" clearable>
<template #prefix-icon><t-icon name="lock-on" /></template>
</t-input>
</div>
<div class="form-item">
<label class="form-label required">Secret Access Key</label>
<t-input v-model="config.minio.secret_access_key" type="password" placeholder="MinIO Secret Key" clearable>
<template #prefix-icon><t-icon name="lock-on" /></template>
</t-input>
</div>
</section>
<!-- Section 3 — Bucket 与选项 -->
<section class="setting-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('settings.storage.bucketSection', 'Bucket') }}</h4>
<div class="form-item">
<label class="form-label required">{{ $t('settings.storage.bucketName') }}</label>
<t-input
v-model="config.minio.bucket_name"
:placeholder="$t('settings.storage.bucketPlaceholder')"
:disabled="config.minio.mode !== 'remote' && !minioEnvAvailable"
clearable
/>
</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
/>
</div>
<div class="form-item">
<label class="form-label">SSL</label>
<div class="vision-toggle">
<t-switch v-model="config.minio.use_ssl" />
<span class="form-desc form-desc--inline">{{ $t('settings.storage.useSslDesc', '通过 HTTPS 访问 MinIO') }}</span>
</div>
</div>
</section>
</template>
<!-- ===== cos ===== -->
<template v-else-if="currentEngine === 'cos'">
<section class="setting-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('settings.storage.credentialsSection', '凭证') }}</h4>
<div class="form-item">
<label class="form-label required">Secret ID</label>
<t-input v-model="config.cos.secret_id" :placeholder="$t('settings.storage.cosSecretIdPlaceholder')" clearable>
<template #prefix-icon><t-icon name="lock-on" /></template>
</t-input>
</div>
<div class="form-item">
<label class="form-label required">Secret Key</label>
<t-input v-model="config.cos.secret_key" type="password" :placeholder="$t('settings.storage.cosSecretKeyPlaceholder')" clearable>
<template #prefix-icon><t-icon name="lock-on" /></template>
</t-input>
</div>
<div class="form-item">
<label class="form-label required">App ID</label>
<t-input v-model="config.cos.app_id" :placeholder="$t('settings.storage.cosAppIdPlaceholder')" clearable />
</div>
</section>
<section class="setting-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('settings.storage.bucketSection', 'Bucket') }}</h4>
<div class="form-item">
<label class="form-label required">Region</label>
<t-input v-model="config.cos.region" placeholder="e.g. ap-guangzhou" clearable />
</div>
<div class="form-item">
<label class="form-label required">{{ $t('settings.storage.bucketName') }}</label>
<t-input v-model="config.cos.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.cos.path_prefix" :placeholder="$t('settings.storage.prefixPlaceholder')" clearable />
</div>
</section>
</template>
<!-- ===== tos ===== -->
<template v-else-if="currentEngine === 'tos'">
<section class="setting-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('settings.storage.credentialsSection', '凭证') }}</h4>
<div class="form-item">
<label class="form-label required">Access Key</label>
<t-input v-model="config.tos.access_key" :placeholder="$t('settings.storage.tosAccessKeyPlaceholder')" clearable>
<template #prefix-icon><t-icon name="lock-on" /></template>
</t-input>
</div>
<div class="form-item">
<label class="form-label required">Secret Key</label>
<t-input v-model="config.tos.secret_key" type="password" :placeholder="$t('settings.storage.tosSecretKeyPlaceholder')" clearable>
<template #prefix-icon><t-icon name="lock-on" /></template>
</t-input>
</div>
</section>
<section class="setting-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('settings.storage.bucketSection', 'Bucket') }}</h4>
<div class="form-item">
<label class="form-label required">Endpoint</label>
<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 required">Region</label>
<t-input v-model="config.tos.region" placeholder="e.g. cn-beijing" clearable />
</div>
<div class="form-item">
<label class="form-label required">{{ $t('settings.storage.bucketName') }}</label>
<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 />
</div>
</section>
</template>
<!-- ===== s3 ===== -->
<template v-else-if="currentEngine === 's3'">
<section class="setting-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('settings.storage.credentialsSection', '凭证') }}</h4>
<div class="form-item">
<label class="form-label required">Access Key</label>
<t-input v-model="config.s3.access_key" :placeholder="$t('settings.storage.s3AccessKeyPlaceholder')" clearable>
<template #prefix-icon><t-icon name="lock-on" /></template>
</t-input>
</div>
<div class="form-item">
<label class="form-label required">Secret Key</label>
<t-input v-model="config.s3.secret_key" type="password" :placeholder="$t('settings.storage.s3SecretKeyPlaceholder')" clearable>
<template #prefix-icon><t-icon name="lock-on" /></template>
</t-input>
</div>
</section>
<section class="setting-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('settings.storage.bucketSection', 'Bucket') }}</h4>
<div class="form-item">
<label class="form-label required">Endpoint</label>
<t-input v-model="config.s3.endpoint" placeholder="e.g. https://s3.amazonaws.com" clearable />
</div>
<div class="form-item">
<label class="form-label required">Region</label>
<t-input v-model="config.s3.region" placeholder="e.g. us-east-1" clearable />
</div>
<div class="form-item">
<label class="form-label required">{{ $t('settings.storage.bucketName') }}</label>
<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 />
</div>
</section>
</template>
<!-- ===== oss ===== -->
<template v-else-if="currentEngine === 'oss'">
<section class="setting-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('settings.storage.credentialsSection', '凭证') }}</h4>
<div class="form-item">
<label class="form-label required">Access Key</label>
<t-input v-model="config.oss.access_key" :placeholder="$t('settings.storage.ossAccessKeyPlaceholder')" clearable>
<template #prefix-icon><t-icon name="lock-on" /></template>
</t-input>
</div>
<div class="form-item">
<label class="form-label required">Secret Key</label>
<t-input v-model="config.oss.secret_key" type="password" :placeholder="$t('settings.storage.ossSecretKeyPlaceholder')" clearable>
<template #prefix-icon><t-icon name="lock-on" /></template>
</t-input>
</div>
</section>
<section class="setting-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('settings.storage.bucketSection', 'Bucket') }}</h4>
<div class="form-item">
<label class="form-label required">Endpoint</label>
<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 required">Region</label>
<t-input v-model="config.oss.region" placeholder="e.g. cn-hangzhou" clearable />
</div>
<div class="form-item">
<label class="form-label required">{{ $t('settings.storage.bucketName') }}</label>
<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 />
</div>
</section>
</template>
<!-- ===== ks3 ===== -->
<template v-else-if="currentEngine === 'ks3'">
<section class="setting-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('settings.storage.credentialsSection', '凭证') }}</h4>
<div class="form-item">
<label class="form-label required">Access Key</label>
<t-input v-model="config.ks3.access_key" :placeholder="$t('settings.storage.ks3AccessKeyPlaceholder')" clearable>
<template #prefix-icon><t-icon name="lock-on" /></template>
</t-input>
</div>
<div class="form-item">
<label class="form-label required">Secret Key</label>
<t-input v-model="config.ks3.secret_key" type="password" :placeholder="$t('settings.storage.ks3SecretKeyPlaceholder')" clearable>
<template #prefix-icon><t-icon name="lock-on" /></template>
</t-input>
</div>
</section>
<section class="setting-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('settings.storage.bucketSection', 'Bucket') }}</h4>
<div class="form-item">
<label class="form-label required">Endpoint</label>
<t-input v-model="config.ks3.endpoint" :placeholder="$t('settings.storage.ks3EndpointPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label required">Region</label>
<t-input v-model="config.ks3.region" :placeholder="$t('settings.storage.ks3RegionPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label required">{{ $t('settings.storage.bucketName') }}</label>
<t-input v-model="config.ks3.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.ks3.path_prefix" :placeholder="$t('settings.storage.prefixPlaceholder')" clearable />
</div>
</section>
</template>
<!-- ===== obs ===== -->
<template v-else-if="currentEngine === 'obs'">
<section class="setting-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('settings.storage.credentialsSection', '凭证') }}</h4>
<div class="form-item">
<label class="form-label required">Access Key</label>
<t-input v-model="config.obs.access_key" :placeholder="$t('settings.storage.obsAccessKeyPlaceholder')" clearable>
<template #prefix-icon><t-icon name="lock-on" /></template>
</t-input>
</div>
<div class="form-item">
<label class="form-label required">Secret Key</label>
<t-input v-model="config.obs.secret_key" type="password" :placeholder="$t('settings.storage.obsSecretKeyPlaceholder')" clearable>
<template #prefix-icon><t-icon name="lock-on" /></template>
</t-input>
</div>
</section>
<section class="setting-drawer__section">
<h4 class="setting-drawer__section-title">{{ $t('settings.storage.bucketSection', 'Bucket') }}</h4>
<div class="form-item">
<label class="form-label required">Endpoint</label>
<t-input v-model="config.obs.endpoint" :placeholder="$t('settings.storage.obsEndpointPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label required">Region</label>
<t-input v-model="config.obs.region" :placeholder="$t('settings.storage.obsRegionPlaceholder')" clearable />
</div>
<div class="form-item">
<label class="form-label required">{{ $t('settings.storage.bucketName') }}</label>
<t-input v-model="config.obs.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.obs.path_prefix" :placeholder="$t('settings.storage.prefixPlaceholder')" clearable />
</div>
</section>
</template>
</div>
</SettingDrawer>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import {
checkStorageEngine,
getStorageEngineConfig,
getStorageEngineStatus,
updateStorageEngineConfig,
type StorageEngineConfig,
} from '@/api/system'
import { useAuthStore } from '@/stores/auth'
import { providerLogo } from './providerLogos'
import SettingDrawer from '@/components/settings/SettingDrawer.vue'
const { t } = useI18n()
const authStore = useAuthStore()
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: '' },
oss: {
endpoint: '',
region: '',
access_key: '',
secret_key: '',
bucket_name: '',
path_prefix: '',
use_temp_bucket: false,
temp_bucket_name: '',
temp_region: '',
},
ks3: {
endpoint: '',
region: '',
access_key: '',
secret_key: '',
bucket_name: '',
path_prefix: '',
},
obs: {
endpoint: '',
region: '',
access_key: '',
secret_key: '',
bucket_name: '',
path_prefix: '',
},
})
const loading = ref(true)
const error = ref('')
const config = ref<StorageEngineConfig>(defaultConfig())
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('')
const saveSuccess = ref(false)
const checkingMinio = ref(false)
const minioCheckResult = ref<{ ok: boolean; message: string; bucket_created?: boolean } | null>(null)
const checkingCos = ref(false)
const cosCheckResult = ref<{ ok: boolean; message: string } | null>(null)
const checkingTos = ref(false)
const tosCheckResult = ref<{ ok: boolean; message: string } | null>(null)
const checkingS3 = ref(false)
const s3CheckResult = ref<{ ok: boolean; message: string } | null>(null)
const checkingOss = ref(false)
const ossCheckResult = ref<{ ok: boolean; message: string } | null>(null)
const checkingKs3 = ref(false)
const ks3CheckResult = ref<{ ok: boolean; message: string } | null>(null)
const checkingObs = ref(false)
const obsCheckResult = 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') },
{ value: 'ks3', label: t('settings.storage.engineKs3'), allowed: isProviderAllowed('ks3') },
{ value: 'obs', label: t('settings.storage.engineObs'), allowed: isProviderAllowed('obs') },
])
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 }
case 'ks3':
return { loading: checkingKs3.value, result: ks3CheckResult.value, onCheck: onCheckKs3 }
case 'obs':
return { loading: checkingObs.value, result: obsCheckResult.value, onCheck: onCheckObs }
default:
return { loading: false, result: null, onCheck: () => undefined }
}
})
const drawerTitle = computed(() => {
if (!currentEngine.value) return ''
const titles: Record<string, string> = {
local: t('settings.storage.localTitle'),
minio: 'MinIO',
cos: t('settings.storage.cosTitle'),
tos: t('settings.storage.tosTitle'),
s3: t('settings.storage.s3Title'),
oss: t('settings.storage.ossTitle'),
ks3: t('settings.storage.ks3Title'),
obs: t('settings.storage.obsTitle'),
}
return titles[currentEngine.value] || currentEngine.value
})
// SettingDrawer 头部图标 — 走列表卡片同款 logo(color / mono / fallback)。
// providerLogo 已经处理了 storage 域内每个 id 的 logo URL + 模式。
const currentLogo = computed(() => {
if (!currentEngine.value) return null
return providerLogo('storage', currentEngine.value as StorageProviderId)
})
// Inline style for the mono mask span. We expose it as a computed instead
// of inlining the literal in the template because Vue's template parser
// chokes on the nested-quote pattern (template-literal inside an object
// literal inside a v-bind expression).
const monoLogoStyle = computed((): Record<string, string> => {
const logo = currentLogo.value
if (!logo || logo.mode !== 'mono') return {}
return { '--logo-url': `url("${logo.url}")` }
})
// 引擎描述(副标题主体文本)。
const engineDescText = computed((): string => {
if (!currentEngine.value) return ''
const key = `settings.storage.${currentEngine.value}Desc`
const translated = t(key)
return translated !== key ? translated : ''
})
// 控制台 / 文档外链 — 副标题尾部 inline 显示,与 ParserEngineSettings
// 同款。各 provider 的 console / docs 链接来自原模板里的硬编码地址。
const ENGINE_LINK_TABLE: Record<string, { console?: string; docs?: string }> = {
cos: {
console: 'https://console.cloud.tencent.com/cos',
docs: 'https://cloud.tencent.com/document/product/436',
},
tos: {
console: 'https://console.volcengine.com/tos',
docs: 'https://www.volcengine.com/docs/6349',
},
s3: {
console: 'https://aws.amazon.com/s3/',
docs: 'https://docs.aws.amazon.com/s3/',
},
oss: {
console: 'https://oss.console.aliyun.com/',
docs: 'https://help.aliyun.com/zh/oss/',
},
obs: {
console: 'https://obs.huaweicloud.com/',
docs: 'https://support.huaweicloud.com/obs/',
},
}
const engineLinks = computed((): Array<{ label: string; url: string }> => {
if (!currentEngine.value) return []
const links = ENGINE_LINK_TABLE[currentEngine.value]
if (!links) return []
const result: Array<{ label: string; url: string }> = []
if (links.console) result.push({ label: t('settings.storage.console'), url: links.console })
if (links.docs) result.push({ label: t('settings.storage.docs'), url: links.docs })
return result
})
// 是否在 footer 显示"测试连接"按钮 — 本地直接读写文件系统,无连接概念,
// 跳过;其余 provider 都需要远程 endpoint,必须能测。
const needsTestButton = computed(() => {
return !!currentEngine.value && currentEngine.value !== 'local'
})
const minioAvailable = computed(() => {
if (config.value.minio?.mode === 'remote') {
return !!(config.value.minio.endpoint && config.value.minio.access_key_id && config.value.minio.secret_access_key)
}
return minioEnvAvailable.value
})
// Single source-of-truth for the cards列 + 状态/标题查询。新增 provider 时
// 在数组里加一项 + 翻译键即可,模板 v-for 自动跟进。
type StorageProviderId = 'local' | 'minio' | 'cos' | 'tos' | 's3' | 'oss' | 'ks3' | 'obs'
const STORAGE_PROVIDERS: { id: StorageProviderId }[] = [
{ id: 'local' },
{ id: 'minio' },
{ id: 'cos' },
{ id: 'tos' },
{ id: 's3' },
{ id: 'oss' },
{ id: 'ks3' },
{ id: 'obs' },
]
const providerTitle = (id: StorageProviderId): string => {
if (id === 'minio') return 'MinIO'
if (id === 's3') return 'AWS S3'
return t(`settings.storage.${id}Title`)
}
const providerInitial = (id: StorageProviderId): string => {
return providerTitle(id).trim().charAt(0).toUpperCase() || '?'
}
// 见 VectorStoreSettings 的同名注释:返回 --logo-url 给 ::before 用 mask 渲染。
const resolveLogo = (id: StorageProviderId) => providerLogo('storage', id)
const badgeClass = (id: StorageProviderId) => {
const m = resolveLogo(id)?.mode
return {
'engine-card__badge--logo': !!m,
'engine-card__badge--color': m === 'color',
'engine-card__badge--mono': m === 'mono',
}
}
const badgeStyle = (id: StorageProviderId): Record<string, string> => {
const logo = resolveLogo(id)
return logo?.mode === 'mono' ? { '--logo-url': `url("${logo.url}")` } : {}
}
const providerStatus = (id: StorageProviderId): { kind: 'on' | 'off'; label: string } => {
if (id === 'minio' && !minioAvailable.value) {
return { kind: 'off', label: t('settings.storage.needsConfig') }
}
if (id === 'local' || id === 'minio') {
return { kind: 'on', label: t('settings.storage.available') }
}
return { kind: 'on', label: t('settings.storage.configurable') }
}
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 = ''
minioCheckResult.value = null
cosCheckResult.value = null
tosCheckResult.value = null
s3CheckResult.value = null
ossCheckResult.value = null
ks3CheckResult.value = null
obsCheckResult.value = null
}
async function loadConfig() {
try {
const res = await getStorageEngineConfig()
const d = res?.data
if (d) {
config.value = {
default_provider: d.default_provider || 'local',
local: d.local ? { path_prefix: d.local.path_prefix || '' } : { path_prefix: '' },
minio: d.minio
? {
mode: d.minio.mode || 'docker',
endpoint: d.minio.endpoint || '',
access_key_id: d.minio.access_key_id || '',
secret_access_key: d.minio.secret_access_key || '',
bucket_name: d.minio.bucket_name || '',
use_ssl: d.minio.use_ssl ?? false,
path_prefix: d.minio.path_prefix || '',
}
: defaultConfig().minio,
cos: d.cos
? {
secret_id: d.cos.secret_id || '',
secret_key: d.cos.secret_key || '',
region: d.cos.region || '',
bucket_name: d.cos.bucket_name || '',
app_id: d.cos.app_id || '',
path_prefix: d.cos.path_prefix || '',
}
: defaultConfig().cos,
tos: d.tos
? {
endpoint: d.tos.endpoint || '',
region: d.tos.region || '',
access_key: d.tos.access_key || '',
secret_key: d.tos.secret_key || '',
bucket_name: d.tos.bucket_name || '',
path_prefix: d.tos.path_prefix || '',
}
: defaultConfig().tos,
s3: d.s3
? {
endpoint: d.s3.endpoint || '',
region: d.s3.region || '',
access_key: d.s3.access_key || '',
secret_key: d.s3.secret_key || '',
bucket_name: d.s3.bucket_name || '',
path_prefix: d.s3.path_prefix || '',
}
: defaultConfig().s3,
oss: d.oss
? {
endpoint: d.oss.endpoint || '',
region: d.oss.region || '',
access_key: d.oss.access_key || '',
secret_key: d.oss.secret_key || '',
bucket_name: d.oss.bucket_name || '',
path_prefix: d.oss.path_prefix || '',
use_temp_bucket: d.oss.use_temp_bucket ?? false,
temp_bucket_name: d.oss.temp_bucket_name || '',
temp_region: d.oss.temp_region || '',
}
: defaultConfig().oss,
ks3: d.ks3
? {
endpoint: d.ks3.endpoint || '',
region: d.ks3.region || '',
access_key: d.ks3.access_key || '',
secret_key: d.ks3.secret_key || '',
bucket_name: d.ks3.bucket_name || '',
path_prefix: d.ks3.path_prefix || '',
}
: defaultConfig().ks3,
obs: d.obs
? {
endpoint: d.obs.endpoint || '',
region: d.obs.region || '',
access_key: d.obs.access_key || '',
secret_key: d.obs.secret_key || '',
bucket_name: d.obs.bucket_name || '',
path_prefix: d.obs.path_prefix || '',
}
: defaultConfig().obs,
}
}
} catch {
config.value = defaultConfig()
}
}
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
if (e.name === 'minio') status.minio = e.available
if (e.name === 'cos') status.cos = e.available
}
engineStatus.value = status
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
}
}
async function loadAll() {
loading.value = true
error.value = ''
try {
await Promise.all([loadConfig(), loadStatus()])
ensureAllowedDefaultProvider()
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : t('settings.storage.loadFailed')
} finally {
loading.value = false
}
}
function buildPayload(): StorageEngineConfig {
const mode = config.value.minio?.mode || 'docker'
return {
default_provider: config.value.default_provider || 'local',
local: { path_prefix: (config.value.local?.path_prefix || '').trim() },
minio: {
mode,
endpoint: mode === 'remote' ? (config.value.minio?.endpoint || '').trim() : '',
access_key_id: mode === 'remote' ? (config.value.minio?.access_key_id || '').trim() : '',
secret_access_key: mode === 'remote' ? (config.value.minio?.secret_access_key || '').trim() : '',
bucket_name: (config.value.minio?.bucket_name || '').trim(),
use_ssl: config.value.minio?.use_ssl ?? false,
path_prefix: (config.value.minio?.path_prefix || '').trim(),
},
cos: {
secret_id: (config.value.cos?.secret_id || '').trim(),
secret_key: (config.value.cos?.secret_key || '').trim(),
region: (config.value.cos?.region || '').trim(),
bucket_name: (config.value.cos?.bucket_name || '').trim(),
app_id: (config.value.cos?.app_id || '').trim(),
path_prefix: (config.value.cos?.path_prefix || '').trim(),
},
tos: {
endpoint: (config.value.tos?.endpoint || '').trim(),
region: (config.value.tos?.region || '').trim(),
access_key: (config.value.tos?.access_key || '').trim(),
secret_key: (config.value.tos?.secret_key || '').trim(),
bucket_name: (config.value.tos?.bucket_name || '').trim(),
path_prefix: (config.value.tos?.path_prefix || '').trim(),
},
s3: {
endpoint: (config.value.s3?.endpoint || '').trim(),
region: (config.value.s3?.region || '').trim(),
access_key: (config.value.s3?.access_key || '').trim(),
secret_key: (config.value.s3?.secret_key || '').trim(),
bucket_name: (config.value.s3?.bucket_name || '').trim(),
path_prefix: (config.value.s3?.path_prefix || '').trim(),
},
oss: {
endpoint: (config.value.oss?.endpoint || '').trim(),
region: (config.value.oss?.region || '').trim(),
access_key: (config.value.oss?.access_key || '').trim(),
secret_key: (config.value.oss?.secret_key || '').trim(),
bucket_name: (config.value.oss?.bucket_name || '').trim(),
path_prefix: (config.value.oss?.path_prefix || '').trim(),
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(),
},
ks3: {
endpoint: (config.value.ks3?.endpoint || '').trim(),
region: (config.value.ks3?.region || '').trim(),
access_key: (config.value.ks3?.access_key || '').trim(),
secret_key: (config.value.ks3?.secret_key || '').trim(),
bucket_name: (config.value.ks3?.bucket_name || '').trim(),
path_prefix: (config.value.ks3?.path_prefix || '').trim(),
},
obs: {
endpoint: (config.value.obs?.endpoint || '').trim(),
region: (config.value.obs?.region || '').trim(),
access_key: (config.value.obs?.access_key || '').trim(),
secret_key: (config.value.obs?.secret_key || '').trim(),
bucket_name: (config.value.obs?.bucket_name || '').trim(),
path_prefix: (config.value.obs?.path_prefix || '').trim(),
},
}
}
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
} catch (e: unknown) {
saveSuccess.value = false
saveMessage.value = e instanceof Error ? e.message : t('settings.storage.saveFailed')
} finally {
saving.value = false
}
}
async function onSaveDefaultEngine() {
await onSave()
}
async function onCheckMinio() {
checkingMinio.value = true
minioCheckResult.value = null
try {
const payload = buildPayload()
const res = await checkStorageEngine({ provider: 'minio', minio: payload.minio })
minioCheckResult.value = res?.data ?? { ok: false, message: t('settings.storage.unknownError') }
} catch (e: unknown) {
minioCheckResult.value = { ok: false, message: e instanceof Error ? e.message : t('settings.storage.requestFailed') }
} finally {
checkingMinio.value = false
}
}
async function onCheckCos() {
checkingCos.value = true
cosCheckResult.value = null
try {
const payload = buildPayload()
const res = await checkStorageEngine({ provider: 'cos', cos: payload.cos })
cosCheckResult.value = res?.data ?? { ok: false, message: t('settings.storage.unknownError') }
} catch (e: unknown) {
cosCheckResult.value = { ok: false, message: e instanceof Error ? e.message : t('settings.storage.requestFailed') }
} finally {
checkingCos.value = false
}
}
async function onCheckTos() {
checkingTos.value = true
tosCheckResult.value = null
try {
const payload = buildPayload()
const res = await checkStorageEngine({ provider: 'tos', tos: payload.tos })
tosCheckResult.value = res?.data ?? { ok: false, message: t('settings.storage.unknownError') }
} catch (e: unknown) {
tosCheckResult.value = { ok: false, message: e instanceof Error ? e.message : t('settings.storage.requestFailed') }
} finally {
checkingTos.value = false
}
}
async function onCheckS3() {
checkingS3.value = true
s3CheckResult.value = null
try {
const payload = buildPayload()
const res = await checkStorageEngine({ provider: 's3', s3: payload.s3 })
s3CheckResult.value = res?.data ?? { ok: false, message: t('settings.storage.unknownError') }
} catch (e: unknown) {
s3CheckResult.value = { ok: false, message: e instanceof Error ? e.message : t('settings.storage.requestFailed') }
} finally {
checkingS3.value = false
}
}
async function onCheckOss() {
checkingOss.value = true
ossCheckResult.value = null
try {
const payload = buildPayload()
const res = await checkStorageEngine({ provider: 'oss', oss: payload.oss })
ossCheckResult.value = res?.data ?? { ok: false, message: t('settings.storage.unknownError') }
} catch (e: unknown) {
ossCheckResult.value = { ok: false, message: e instanceof Error ? e.message : t('settings.storage.requestFailed') }
} finally {
checkingOss.value = false
}
}
async function onCheckKs3() {
checkingKs3.value = true
ks3CheckResult.value = null
try {
const payload = buildPayload()
const res = await checkStorageEngine({ provider: 'ks3', ks3: payload.ks3 })
ks3CheckResult.value = res?.data ?? { ok: false, message: t('settings.storage.unknownError') }
} catch (e: unknown) {
ks3CheckResult.value = { ok: false, message: e instanceof Error ? e.message : t('settings.storage.requestFailed') }
} finally {
checkingKs3.value = false
}
}
async function onCheckObs() {
checkingObs.value = true
obsCheckResult.value = null
try {
const payload = buildPayload()
const res = await checkStorageEngine({ provider: 'obs', obs: payload.obs })
obsCheckResult.value = res?.data ?? { ok: false, message: t('settings.storage.unknownError') }
} catch (e: unknown) {
obsCheckResult.value = { ok: false, message: e instanceof Error ? e.message : t('settings.storage.requestFailed') }
} finally {
checkingObs.value = false
}
}
onMounted(loadAll)
</script>
<style lang="less" scoped>
.storage-engine-settings {
width: 100%;
}
.section-header {
margin-bottom: 32px;
h2 {
font-size: 20px;
font-weight: 600;
color: var(--td-text-color-primary);
margin: 0 0 8px 0;
}
.section-description {
font-size: 14px;
color: var(--td-text-color-secondary);
margin: 0;
line-height: 1.5;
}
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 48px 0;
color: var(--td-text-color-placeholder);
font-size: 14px;
}
.error-inline {
padding: 16px 0;
}
.settings-group {
display: flex;
flex-direction: column;
gap: 0;
}
.setting-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 20px 0;
border-bottom: 1px solid var(--td-component-stroke);
&:last-child {
border-bottom: none;
}
}
.setting-info {
flex: 1;
max-width: 65%;
padding-right: 24px;
label {
font-size: 15px;
font-weight: 500;
color: var(--td-text-color-primary);
display: block;
margin-bottom: 4px;
}
.desc {
font-size: 13px;
color: var(--td-text-color-secondary);
margin: 0;
line-height: 1.5;
}
}
.setting-control {
flex-shrink: 0;
min-width: 280px;
display: flex;
justify-content: flex-end;
align-items: center;
}
.engine-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 12px;
margin-top: 24px;
}
// 与 Parser / Model / WebSearch / Mcp 一致的卡片样式 —— 整张是 button,
// 单击打开抽屉;active 是「当前正在编辑」的语义而不是「默认引擎」。
.engine-card {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px 14px 14px 12px;
border: 1px solid var(--td-component-stroke);
border-radius: 10px;
background: var(--td-bg-color-container);
text-align: left;
font: inherit;
color: inherit;
cursor: pointer;
transition: border-color 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;
min-width: 0;
&:hover {
border-color: var(--td-brand-color-3, var(--td-brand-color));
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06);
}
&--active {
border-color: var(--td-brand-color);
background: var(--td-brand-color-1, rgba(7, 192, 95, 0.06));
}
}
.engine-card__badge {
flex-shrink: 0;
width: 36px;
height: 36px;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 1px;
font-size: 15px;
font-weight: 600;
letter-spacing: 0.02em;
background: rgba(0, 82, 217, 0.1);
color: #0052D9;
}
// 真实品牌 logo:白底 + 细边,logo 用 mask-image 染成 currentColor(沿用品牌色)。
// 多套一层 .engine-card 以胜过 `.engine-card--<id> .engine-card__badge` 的具体规则。
.engine-card .engine-card__badge--logo {
background: var(--td-bg-color-container, #fff);
box-shadow: inset 0 0 0 1px var(--td-component-stroke);
}
.engine-card .engine-card__badge--mono::before {
content: '';
width: 22px;
height: 22px;
background-color: currentColor;
-webkit-mask-image: var(--logo-url);
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
mask-image: var(--logo-url);
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
}
.engine-card__badge-img {
width: 24px;
height: 24px;
object-fit: contain;
display: block;
}
// 各对象存储徽章配色 —— 和 LOGO 主色对齐,但走低饱和版以维持 settings 整体调性。
.engine-card--local .engine-card__badge {
background: rgba(70, 70, 70, 0.1);
color: #464646;
}
.engine-card--minio .engine-card__badge {
background: rgba(225, 38, 38, 0.12);
color: #C0382B;
}
.engine-card--cos .engine-card__badge {
background: rgba(0, 82, 217, 0.1);
color: #0052D9;
}
.engine-card--tos .engine-card__badge {
background: rgba(0, 137, 255, 0.12);
color: #0089FF;
}
.engine-card--s3 .engine-card__badge {
background: rgba(255, 153, 0, 0.12);
color: #D97706;
}
.engine-card--oss .engine-card__badge {
background: rgba(255, 90, 0, 0.12);
color: #E55A00;
}
.engine-card--ks3 .engine-card__badge {
background: rgba(7, 192, 95, 0.12);
color: #07A050;
}
.engine-card--obs .engine-card__badge {
background: rgba(206, 17, 38, 0.1);
color: #CE1126;
}
.engine-card__body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.engine-card__header {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.engine-card__title {
flex: 1;
min-width: 0;
margin: 0;
font-size: 14px;
font-weight: 600;
line-height: 1.4;
color: var(--td-text-color-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.engine-card__status {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 5px;
padding: 1px 8px 1px 6px;
font-size: 11px;
font-weight: 500;
line-height: 16px;
border-radius: 10px;
background: var(--td-bg-color-secondarycontainer);
&--on {
color: var(--td-success-color-7, #118053);
.engine-card__status-dot { background: var(--td-success-color, #118053); }
}
&--off {
color: var(--td-text-color-placeholder);
.engine-card__status-dot { background: var(--td-gray-color-5); }
}
}
.engine-card__status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.engine-card__desc {
font-size: 12px;
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;
}
// ---- 抽屉内容 — 与 ModelEditorDialog 同款约定 ----
// color logo 容器内部的图片
.header-icon__img {
width: 24px;
height: 24px;
object-fit: contain;
display: block;
}
// mono logo(用 mask-image 渲染,颜色由 currentColor 决定)
.header-icon__mono {
display: inline-block;
width: 22px;
height: 22px;
background-color: currentColor;
-webkit-mask-image: var(--logo-url);
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
mask-image: var(--logo-url);
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
}
// fallback:首字母 monogram
.header-icon__text {
font-size: 15px;
font-weight: 600;
letter-spacing: 0.02em;
}
.form-item {
margin-bottom: 0;
}
.form-label {
display: block;
margin-bottom: 6px;
font-size: 13px;
font-weight: 500;
color: var(--td-text-color-primary);
line-height: 1.4;
// 必填星号前置(与 ModelEditorDialog 一致)
&.required::before {
content: '*';
color: var(--td-error-color);
margin-right: 4px;
font-weight: 500;
line-height: 1;
}
}
.form-desc {
margin: 4px 0 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--td-text-color-placeholder);
&--inline {
margin: 0;
}
}
:deep(.t-input),
:deep(.t-select),
:deep(.t-textarea),
:deep(.t-input-number) {
width: 100%;
font-size: 13px;
}
// ---- MinIO 部署模式:紧凑 pill segmented(与 ModelEditorDialog 来源切换同款)----
.source-options {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px;
background: var(--td-bg-color-component);
border: 1px solid var(--td-component-stroke);
border-radius: 8px;
}
.source-option {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
height: 28px;
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
cursor: pointer;
font-family: inherit;
font-size: 13px;
color: var(--td-text-color-secondary);
line-height: 1;
transition: all 0.15s ease;
&:hover:not(.is-active) {
color: var(--td-text-color-primary);
background: var(--td-bg-color-container-hover);
}
&.is-active {
background: var(--td-bg-color-container);
border-color: var(--td-brand-color);
color: var(--td-brand-color);
font-weight: 500;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
}
.source-option__icon {
font-size: 14px;
flex-shrink: 0;
}
.source-option__label {
white-space: nowrap;
}
// ---- inline-alert(与 ParserEngineSettings 同款,瘦身的状态行) ----
.inline-alert {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
font-size: 13px;
line-height: 1.5;
color: var(--td-text-color-secondary);
flex-wrap: wrap;
}
.inline-alert__icon {
font-size: 15px;
flex-shrink: 0;
color: var(--td-text-color-placeholder);
}
.inline-alert--ok .inline-alert__icon {
color: var(--td-success-color);
}
.inline-alert--warn {
color: var(--td-text-color-primary);
.inline-alert__icon {
color: var(--td-warning-color, #f97316);
}
}
.inline-alert__text {
flex: 1 1 auto;
min-width: 0;
}
// ---- vision-toggle(switch + 行内描述)----
.vision-toggle {
display: flex;
align-items: center;
gap: 8px;
}
// ---- footer-left 测试连接消息(与 ModelEditorDialog 同款) ----
.footer-test-message {
font-size: 12px;
line-height: 1.4;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.success {
color: var(--td-brand-color-active);
}
&.error {
color: var(--td-error-color);
}
&.created {
color: var(--td-warning-color, #f97316);
}
}
.status-icon {
font-size: 16px;
flex-shrink: 0;
&.available {
color: var(--td-brand-color);
}
&.unavailable {
color: var(--td-error-color);
}
}
// ---- 文档外链 ----
.doc-link {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 13px;
font-weight: 500;
color: var(--td-brand-color);
text-decoration: none;
transition: color 0.15s ease;
&:hover {
color: var(--td-brand-color-active);
}
.link-icon {
font-size: 14px;
}
&--inline {
margin-left: 6px;
font-size: 12px;
font-weight: 500;
vertical-align: baseline;
.link-icon {
font-size: 12px;
}
}
}
.save-msg {
font-size: 13px;
&.success {
color: var(--td-success-color);
}
&.error {
color: var(--td-error-color);
}
}
</style>
<!--
Non-scoped block: per-engine header icon coloring. The drawer panel is
rendered into the component tree (no teleport since attach is unset),
but TDesign's t-drawer can in some builds reuse a shared root that
drops scoped data attributes — keep these rules global so the mapping
always lands. Namespaced under .storage-engine-drawer--{id} so it
cannot bleed into other consumers of SettingDrawer.
Each rule mirrors the matching .engine-card--{id} .engine-card__badge
background + color pair from the scoped block above, so the list-card
→ drawer hand-off is visually continuous.
-->
<style lang="less">
// 当抽屉里渲染了彩色 logo(如 MinIO/AWS)时,给 header-icon 容器一个白底
// + 细边,避免品牌色浅底盖在彩色图标上影响对比度。
.storage-engine-drawer .setting-drawer__header-icon:has(.header-icon__img) {
background: var(--td-bg-color-container, #fff);
box-shadow: inset 0 0 0 1px var(--td-component-stroke);
}
// 单色 logo / 首字母 fallback 的徽章配色 — 与列表卡片 .engine-card--{id}
// .engine-card__badge 完全一致。
.storage-engine-drawer--local .setting-drawer__header-icon {
background: rgba(70, 70, 70, 0.1);
color: #464646;
}
.storage-engine-drawer--minio .setting-drawer__header-icon {
background: rgba(225, 38, 38, 0.12);
color: #C0382B;
}
.storage-engine-drawer--cos .setting-drawer__header-icon {
background: rgba(0, 82, 217, 0.1);
color: #0052D9;
}
.storage-engine-drawer--tos .setting-drawer__header-icon {
background: rgba(0, 137, 255, 0.12);
color: #0089FF;
}
.storage-engine-drawer--s3 .setting-drawer__header-icon {
background: rgba(255, 153, 0, 0.12);
color: #D97706;
}
.storage-engine-drawer--oss .setting-drawer__header-icon {
background: rgba(255, 90, 0, 0.12);
color: #E55A00;
}
.storage-engine-drawer--ks3 .setting-drawer__header-icon {
background: rgba(7, 192, 95, 0.12);
color: #07A050;
}
.storage-engine-drawer--obs .setting-drawer__header-icon {
background: rgba(206, 17, 38, 0.1);
color: #CE1126;
}
</style>