From 90773ab69f8f17aff97e7544c2ed84a2dd81c51a Mon Sep 17 00:00:00 2001 From: zhayujie Date: Thu, 21 May 2026 20:22:09 +0800 Subject: [PATCH] feat(models): allow viewing and editing search vendor credentials --- channel/web/chat.html | 7 +- channel/web/static/js/console.js | 160 +++++++++++++++++++++++-------- channel/web/web_channel.py | 9 +- 3 files changed, 131 insertions(+), 45 deletions(-) diff --git a/channel/web/chat.html b/channel/web/chat.html index 455adca3..947e07b7 100644 --- a/channel/web/chat.html +++ b/channel/web/chat.html @@ -474,6 +474,11 @@

模型配置

+ + 高级配置 + +
@@ -1084,7 +1089,7 @@
diff --git a/channel/web/static/js/console.js b/channel/web/static/js/console.js index e067c93e..09289dcb 100644 --- a/channel/web/static/js/console.js +++ b/channel/web/static/js/console.js @@ -58,7 +58,8 @@ const I18N = { models_search_add_provider: '添加厂商', models_search_add_desc: '选择一个搜索厂商进行配置', models_search_bocha_title: '配置博查 API Key', - models_search_bocha_desc: '前往博查开放平台创建 API Key:', + models_search_bocha_desc: '前往 博查开放平台 创建 API Key。', + models_search_edit_hint: '点击修改配置', models_unavailable: '不可用', models_set_via_env: '通过环境变量启用', models_dim_label: '维度', @@ -93,6 +94,7 @@ const I18N = { input_placeholder: '输入消息,或输入 / 使用指令', config_title: '配置管理', config_desc: '管理模型和 Agent 配置', config_model: '模型配置', config_agent: 'Agent 配置', + config_model_advanced: '高级配置', config_channel: '通道配置', config_agent_enabled: 'Agent 模式', config_max_tokens: '最大上下文 Token', config_max_tokens_hint: '对话中 Agent 能输入的最大 Token 长度,超过后会智能压缩处理', @@ -230,7 +232,8 @@ const I18N = { models_search_add_provider: 'Add provider', models_search_add_desc: 'Pick a search provider to configure', models_search_bocha_title: 'Configure Bocha API Key', - models_search_bocha_desc: 'Create a key at the Bocha open platform: ', + models_search_bocha_desc: 'Create a key at the Bocha open platform.', + models_search_edit_hint: 'Click to edit', models_unavailable: 'unavailable', models_set_via_env: 'enable via environment variable', models_dim_label: 'dim', @@ -265,6 +268,7 @@ const I18N = { input_placeholder: 'Type a message, or press / for commands', config_title: 'Configuration', config_desc: 'Manage model and agent settings', config_model: 'Model Configuration', config_agent: 'Agent Configuration', + config_model_advanced: 'Advanced', config_channel: 'Channel Configuration', config_agent_enabled: 'Agent Mode', config_max_tokens: 'Max Context Tokens', config_max_tokens_hint: 'Max tokens the Agent can input per conversation, auto-compressed when exceeded', @@ -4094,10 +4098,13 @@ function _renderSearchSummary(body, cap) { `; } else { const chips = configured.map(p => ` - + `).join(''); host.innerHTML = `
@@ -4108,13 +4115,21 @@ function _renderSearchSummary(body, cap) { `; } - const btn = host.querySelector('#cap-search-add-btn'); - if (btn) { - btn.addEventListener('click', (ev) => { + const addBtnEl = host.querySelector('#cap-search-add-btn'); + if (addBtnEl) { + addBtnEl.addEventListener('click', (ev) => { ev.preventDefault(); openSearchAddProviderPicker(missing); }); } + host.querySelectorAll('[data-search-edit-provider]').forEach(el => { + el.addEventListener('click', (ev) => { + ev.preventDefault(); + const pid = el.getAttribute('data-search-edit-provider'); + const meta = (cap.providers || []).find(p => p.id === pid); + _launchSearchProviderConfig(pid, meta); + }); + }); } // Two-step add flow: click "+ 添加厂商" -> chooser dialog -> per-provider @@ -4168,9 +4183,9 @@ function openSearchAddProviderPicker(missingProviders) { }); } -function _launchSearchProviderConfig(providerId) { +function _launchSearchProviderConfig(providerId, providerMeta) { if (providerId === 'bocha') { - openSearchBochaModal(); + openSearchBochaModal(providerMeta); } else { openVendorModal(providerId, () => loadModelsView({ preserveScroll: true })); } @@ -4204,54 +4219,106 @@ function saveSearchCapability() { // Minimal bocha API-key modal. Reuses the existing vendor-modal markup // helpers would be nice, but bocha isn't in PROVIDER_MODELS (it's not a // model vendor), so we render a tiny dedicated dialog. -function openSearchBochaModal() { +function openSearchBochaModal(providerMeta) { const existing = document.getElementById('search-bocha-modal'); if (existing) existing.remove(); + let masked = (providerMeta && providerMeta.api_key_masked) || ''; + if (!masked) { + const searchCap = (modelsState && modelsState.capabilities && modelsState.capabilities.search) || {}; + const bocha = (searchCap.providers || []).find(p => p.id === 'bocha'); + if (bocha && bocha.api_key_masked) masked = bocha.api_key_masked; + } + const hasKey = !!masked; + const clearBtnHtml = hasKey + ? `` + : ''; + const modal = document.createElement('div'); modal.id = 'search-bocha-modal'; modal.className = 'fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm'; modal.innerHTML = ` -

${t('models_search_bocha_title')}

-

- ${t('models_search_bocha_desc')} - open.bochaai.com -

+

${t('models_search_bocha_desc')}

- -
- - +
+
${clearBtnHtml}
+
+ + +
`; document.body.appendChild(modal); - setTimeout(() => { - const input = document.getElementById('search-bocha-key'); - if (input) input.focus(); - }, 50); + + // Reset masked sentinel as soon as the user starts editing so the save + // handler can tell apart "kept the existing key" vs "typed a new one". + const input = document.getElementById('search-bocha-key'); + if (input) { + const unmask = () => { + if (input.dataset.masked === '1') { + input.value = ''; + input.dataset.masked = ''; + input.classList.remove('cfg-key-masked'); + } + }; + input.addEventListener('keydown', (e) => { + if (e.key === 'Tab' || e.key === 'Escape') return; + unmask(); + }); + input.addEventListener('paste', unmask); + if (!hasKey) setTimeout(() => input.focus(), 50); + } + const clearBtn = document.getElementById('search-bocha-clear'); + if (clearBtn) clearBtn.addEventListener('click', _clearBochaKey); + + modal.addEventListener('mousedown', (e) => { + if (e.target === modal) modal.remove(); + }); + const onKey = (e) => { + if (e.key === 'Escape') { + modal.remove(); + document.removeEventListener('keydown', onKey); + } + }; + document.addEventListener('keydown', onKey); } function _saveBochaKey() { const input = document.getElementById('search-bocha-key'); - const apiKey = input ? input.value.trim() : ''; + if (!input) return; + // Untouched masked value => no change requested; close silently. + if (input.dataset.masked === '1') { + const modal = document.getElementById('search-bocha-modal'); + if (modal) modal.remove(); + return; + } + const apiKey = input.value.trim(); if (!apiKey) { - if (input) input.focus(); + input.focus(); return; } fetch('/api/models', { @@ -4267,6 +4334,20 @@ function _saveBochaKey() { }); } +function _clearBochaKey() { + fetch('/api/models', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'set_search_credential', api_key: '' }), + }).then(r => r.json()).then(data => { + if (data.status === 'success') { + const modal = document.getElementById('search-bocha-modal'); + if (modal) modal.remove(); + loadModelsView({ preserveScroll: true }); + } + }); +} + function renderCapabilityBody(def, cap, body) { if (def.id === 'search') { renderSearchCapability(def, cap, body); @@ -5042,12 +5123,7 @@ function fillVendorModalForProvider(providerId) { baseWrap.classList.remove('hidden'); baseInput.placeholder = meta.api_base_default || meta.api_base_placeholder || ''; baseInput.value = meta.api_base || ''; - if (meta.api_base_default) { - baseHint.classList.remove('hidden'); - baseHint.querySelector('span').textContent = `${t('models_base_default')}: ${meta.api_base_default}`; - } else { - baseHint.classList.add('hidden'); - } + baseHint.classList.add('hidden'); } else { baseWrap.classList.add('hidden'); baseInput.value = ''; diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py index f5d5d724..4adcefc7 100644 --- a/channel/web/web_channel.py +++ b/channel/web/web_channel.py @@ -2252,6 +2252,7 @@ class ModelsHandler: configured_ids = [] for pid in cls._SEARCH_PROVIDERS: ok = cls._is_real_key(cls._search_provider_key(pid, local_config)) + raw_key = cls._search_provider_key(pid, local_config) if ok else "" providers.append({ "id": pid, "label": cls._SEARCH_PROVIDER_LABELS.get(pid, pid), @@ -2260,6 +2261,7 @@ class ModelsHandler: # piggy-back on a model-vendor credential. Frontend uses # this hint to decide which credential editor to surface. "needs_dedicated_key": pid == "bocha", + "api_key_masked": ConfigHandler._mask_key(raw_key) if raw_key else "", }) if ok: configured_ids.append(pid) @@ -2394,8 +2396,11 @@ class ModelsHandler: for field_name in (meta.get("api_key_field"), meta.get("api_base_key")): if not field_name: continue - if field_name in local_config: - local_config[field_name] = "" + # Always write the key — even if it was absent before — so the + # in-memory conf() reflects the cleared state without needing a + # restart. (`in local_config` was too strict: provider keys that + # were ever set then deleted manually wouldn't get reset.) + local_config[field_name] = "" file_cfg[field_name] = "" cleared.append(field_name)