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 @@
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)