fix: show websearch API key visibility/copy buttons for saved providers

The buttons were hidden because v-if only checked provider.api_key,
which is always empty for saved providers (backend sanitizes it).
Now also checks api_key_configured. Copy button is disabled when
no actual key is available (only configured placeholder shown).
This commit is contained in:
erio
2026-04-14 07:22:22 +08:00
parent b402c367d3
commit 9e0d12d3b0
4 changed files with 39 additions and 10 deletions

View File

@@ -1 +1 @@
0.1.110.51 0.1.112.3

View File

@@ -1939,7 +1939,7 @@ func (h *SettingHandler) GetWebSearchEmulationConfig(c *gin.Context) {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
} }
response.Success(c, service.SanitizeWebSearchConfig(c.Request.Context(), cfg)) response.Success(c, service.PopulateWebSearchUsage(c.Request.Context(), cfg))
} }
// UpdateWebSearchEmulationConfig 更新 Web Search 模拟配置 // UpdateWebSearchEmulationConfig 更新 Web Search 模拟配置

View File

@@ -277,6 +277,28 @@ func TestWebSearch(ctx context.Context, query string) (*WebSearchTestResult, err
}, nil }, nil
} }
// PopulateWebSearchUsage returns a copy with quota usage populated from Redis (api_key kept as-is).
func PopulateWebSearchUsage(ctx context.Context, cfg *WebSearchEmulationConfig) *WebSearchEmulationConfig {
if cfg == nil {
return nil
}
out := *cfg
out.Providers = make([]WebSearchProviderConfig, len(cfg.Providers))
mgr := getWebSearchManager()
for i, p := range cfg.Providers {
out.Providers[i] = p
out.Providers[i].APIKeyConfigured = p.APIKey != ""
if mgr != nil {
used, _ := mgr.GetUsage(ctx, p.Type)
out.Providers[i].QuotaUsed = used
}
}
return &out
}
// SanitizeWebSearchConfig returns a copy with api_key fields masked and quota usage populated. // SanitizeWebSearchConfig returns a copy with api_key fields masked and quota usage populated.
func SanitizeWebSearchConfig(ctx context.Context, cfg *WebSearchEmulationConfig) *WebSearchEmulationConfig { func SanitizeWebSearchConfig(ctx context.Context, cfg *WebSearchEmulationConfig) *WebSearchEmulationConfig {
if cfg == nil { if cfg == nil {

View File

@@ -1775,8 +1775,8 @@
@click.stop @click.stop
/> />
<!-- Quota summary in collapsed state --> <!-- Quota summary in collapsed state -->
<span v-if="!expandedProviders[pIdx] && provider.quota_limit > 0" class="text-xs text-gray-400"> <span v-if="!expandedProviders[pIdx]" class="text-xs text-gray-400">
{{ provider.quota_used ?? 0 }} / {{ provider.quota_limit }} {{ provider.quota_used ?? 0 }} / {{ provider.quota_limit > 0 ? provider.quota_limit : '∞' }}
</span> </span>
<span v-if="!expandedProviders[pIdx] && provider.api_key_configured" class="text-xs text-green-500"> <span v-if="!expandedProviders[pIdx] && provider.api_key_configured" class="text-xs text-green-500">
{{ t('admin.settings.webSearchEmulation.apiKeyConfigured') }} {{ t('admin.settings.webSearchEmulation.apiKeyConfigured') }}
@@ -1797,10 +1797,10 @@
v-model="provider.api_key" v-model="provider.api_key"
:type="apiKeyVisible[pIdx] ? 'text' : 'password'" :type="apiKeyVisible[pIdx] ? 'text' : 'password'"
class="input w-full text-sm" class="input w-full text-sm"
:class="provider.api_key ? 'pr-16' : ''" :class="(provider.api_key || provider.api_key_configured) ? 'pr-16' : ''"
:placeholder="provider.api_key_configured ? '••••••••' : t('admin.settings.webSearchEmulation.apiKeyPlaceholder')" :placeholder="provider.api_key_configured ? '••••••••' : t('admin.settings.webSearchEmulation.apiKeyPlaceholder')"
/> />
<div v-if="provider.api_key" class="absolute inset-y-0 right-0 flex items-center pr-1.5"> <div v-if="provider.api_key || provider.api_key_configured" class="absolute inset-y-0 right-0 flex items-center pr-1.5">
<button <button
type="button" type="button"
class="rounded p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" class="rounded p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
@@ -1818,7 +1818,9 @@
<button <button
type="button" type="button"
class="rounded p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" class="rounded p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
:class="{ 'opacity-30 cursor-not-allowed': !provider.api_key }"
:title="t('admin.settings.webSearchEmulation.copyApiKey')" :title="t('admin.settings.webSearchEmulation.copyApiKey')"
:disabled="!provider.api_key"
@click="copyApiKey(pIdx)" @click="copyApiKey(pIdx)"
> >
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -1849,16 +1851,17 @@
</div> </div>
<!-- Usage display --> <!-- Usage display -->
<div v-if="provider.quota_limit > 0" class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-xs text-gray-500">{{ t('admin.settings.webSearchEmulation.quotaUsage') }}:</span> <span class="text-xs text-gray-500">{{ t('admin.settings.webSearchEmulation.quotaUsage') }}:</span>
<div class="flex-1 rounded-full bg-gray-200 dark:bg-dark-600" style="height: 6px"> <div v-if="provider.quota_limit > 0" class="flex-1 rounded-full bg-gray-200 dark:bg-dark-600" style="height: 6px">
<div <div
class="h-full rounded-full transition-all" class="h-full rounded-full transition-all"
:class="quotaPercentage(provider) > 90 ? 'bg-red-500' : quotaPercentage(provider) > 70 ? 'bg-yellow-500' : 'bg-green-500'" :class="quotaPercentage(provider) > 90 ? 'bg-red-500' : quotaPercentage(provider) > 70 ? 'bg-yellow-500' : 'bg-green-500'"
:style="{ width: Math.min(quotaPercentage(provider), 100) + '%' }" :style="{ width: Math.min(quotaPercentage(provider), 100) + '%' }"
/> />
</div> </div>
<span class="text-xs text-gray-500">{{ provider.quota_used ?? 0 }} / {{ provider.quota_limit }}</span> <div v-else class="flex-1" />
<span class="text-xs text-gray-500">{{ provider.quota_used ?? 0 }} / {{ provider.quota_limit > 0 ? provider.quota_limit : '∞' }}</span>
</div> </div>
<!-- Proxy + Test on same row --> <!-- Proxy + Test on same row -->
@@ -3164,9 +3167,13 @@ async function loadWebSearchConfig() {
async function saveWebSearchConfig(): Promise<boolean> { async function saveWebSearchConfig(): Promise<boolean> {
try { try {
const providers = webSearchConfig.providers.map((p: WebSearchProviderConfig) => ({
...p,
quota_limit: typeof p.quota_limit === 'number' && p.quota_limit > 0 ? p.quota_limit : 0,
}))
await adminAPI.settings.updateWebSearchEmulationConfig({ await adminAPI.settings.updateWebSearchEmulationConfig({
enabled: webSearchConfig.enabled, enabled: webSearchConfig.enabled,
providers: webSearchConfig.providers as WebSearchProviderConfig[], providers,
}) })
return true return true
} catch (err: unknown) { } catch (err: unknown) {