From 84cbaa57dcab1a70c8634983f5716068d4d9c091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:31:38 +0800 Subject: [PATCH] Add files via upload --- web/static/css/style.css | 207 ++++++++++++++++++++ web/static/i18n/en-US.json | 25 +++ web/static/i18n/zh-CN.json | 25 +++ web/static/js/settings.js | 377 ++++++++++++++++++++++++++++++++++++- web/templates/index.html | 90 +++++++++ 5 files changed, 721 insertions(+), 3 deletions(-) diff --git a/web/static/css/style.css b/web/static/css/style.css index a64b5ccc..5a0ad753 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -7116,6 +7116,170 @@ html[data-theme="dark"] .openapi-doc-btn:hover { padding-right: 36px; } +.settings-native-select { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; + opacity: 0 !important; + pointer-events: none !important; +} + +.settings-custom-select { + position: relative; + width: 100%; + min-width: 0; +} + +.settings-custom-select--compact { + width: auto; + flex: 0 0 auto; +} + +.settings-custom-select-trigger { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + width: 100%; + min-height: 42px; + box-sizing: border-box; + padding: 9px 12px; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--bg-primary); + color: var(--text-primary); + font: inherit; + font-size: 0.9375rem; + line-height: 1.25; + cursor: pointer; + transition: border-color 0.15s ease, box-shadow 0.15s ease, background-color 0.15s ease; +} + +.settings-custom-select--compact .settings-custom-select-trigger { + min-height: 34px; + padding: 6px 10px; + font-size: 0.875rem; +} + +.settings-custom-select-value { + flex: 1; + min-width: 0; + overflow: hidden; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; +} + +.settings-custom-select-caret { + flex: 0 0 auto; + color: var(--text-secondary); + font-size: 0.75rem; + line-height: 1; + transition: transform 0.15s ease; +} + +.settings-custom-select.open .settings-custom-select-caret { + transform: rotate(180deg); +} + +.settings-custom-select-trigger:hover:not(:disabled) { + border-color: var(--accent-color); +} + +.settings-custom-select.open .settings-custom-select-trigger { + border-color: var(--accent-color); + box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1); +} + +.settings-custom-select.is-disabled .settings-custom-select-trigger { + cursor: not-allowed; + opacity: 0.56; + background: var(--bg-secondary, #f5f6f8); +} + +.settings-custom-select-menu { + display: none; + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + z-index: 2600; + max-height: 280px; + overflow-y: auto; + padding: 6px; + border: 1px solid var(--border-color); + border-radius: 8px; + background: var(--bg-primary); + box-shadow: var(--shadow-lg); +} + +.settings-custom-select.open .settings-custom-select-menu { + display: block; +} + +.settings-custom-select-option { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + box-sizing: border-box; + padding: 8px 10px; + border: 0; + border-radius: 6px; + background: transparent; + color: var(--text-primary); + font: inherit; + font-size: 0.875rem; + line-height: 1.25; + text-align: left; + cursor: pointer; + transition: background-color 0.12s ease, color 0.12s ease; +} + +.settings-custom-select-option:hover:not(:disabled) { + background: var(--bg-secondary); +} + +.settings-custom-select-option.is-selected { + color: var(--accent-color); + font-weight: 600; + background: rgba(0, 102, 255, 0.08); +} + +.settings-custom-select-option.is-disabled, +.settings-custom-select-option:disabled { + cursor: not-allowed; + opacity: 0.45; +} + +.settings-custom-select-check { + flex: 0 0 14px; + width: 14px; + color: var(--accent-color); + font-size: 0.75rem; + line-height: 1; + opacity: 0; + text-align: center; +} + +.settings-custom-select-option.is-selected .settings-custom-select-check { + opacity: 1; +} + +.settings-custom-select-label { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .form-group input:not([type="checkbox"]):not([type="radio"]):focus, .form-group select:focus { outline: none; @@ -30314,6 +30478,49 @@ html[data-theme="dark"] #page-settings select option { color: var(--text-primary); } +html[data-theme="dark"] #page-settings .settings-custom-select-trigger, +html[data-theme="dark"] #page-settings .audit-custom-select-trigger { + background: #0f172a !important; + color: var(--text-primary) !important; + border-color: #2b374b !important; + box-shadow: none !important; +} + +html[data-theme="dark"] #page-settings .settings-custom-select-trigger:hover:not(:disabled), +html[data-theme="dark"] #page-settings .audit-custom-select-trigger:hover:not(:disabled) { + border-color: rgba(96, 165, 250, 0.75) !important; +} + +html[data-theme="dark"] #page-settings .settings-custom-select.open .settings-custom-select-trigger, +html[data-theme="dark"] #page-settings .audit-custom-select.open .audit-custom-select-trigger { + background: #111827 !important; + border-color: var(--accent-color) !important; + box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.16) !important; +} + +html[data-theme="dark"] #page-settings .settings-custom-select-menu, +html[data-theme="dark"] #page-settings .audit-custom-select-dropdown { + background: #0f172a !important; + border-color: #2b374b !important; + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.42) !important; +} + +html[data-theme="dark"] #page-settings .settings-custom-select-option, +html[data-theme="dark"] #page-settings .audit-custom-select-option { + color: var(--text-primary) !important; +} + +html[data-theme="dark"] #page-settings .settings-custom-select-option:hover:not(:disabled), +html[data-theme="dark"] #page-settings .audit-custom-select-option:hover { + background: rgba(96, 165, 250, 0.12) !important; +} + +html[data-theme="dark"] #page-settings .settings-custom-select-option.is-selected, +html[data-theme="dark"] #page-settings .audit-custom-select-option.is-selected { + background: rgba(96, 165, 250, 0.16) !important; + color: #60a5fa !important; +} + /* Chat @ tool mention panel dark theme. */ html[data-theme="dark"] .mention-suggestions { background: #111827 !important; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 4a79249f..10102447 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -1221,6 +1221,7 @@ "title": "System settings", "nav": { "basic": "Basic", + "hitl": "Human-in-the-loop", "knowledge": "Knowledge base", "c2": "C2", "robots": "Bots", @@ -1232,6 +1233,30 @@ "infocollect": { "title": "Reconnaissance" }, + "hitl": { + "title": "Human-in-the-loop", + "approvalTitle": "Approval", + "defaultReviewer": "Global default reviewer", + "defaultReviewerHint": "Used when no conversation is selected and for new conversations; the chat sidebar can still override it.", + "auditModelTitle": "Audit Agent model", + "auditModelReuseMain": "Follow main model config", + "auditModelBaseUrlPlaceholder": "Leave blank to reuse the main Base URL", + "auditModelApiKeyPlaceholder": "Leave blank to reuse the main API Key", + "auditModelName": "Approval model", + "auditModelNamePlaceholder": "Leave blank to reuse the main model; a small model is recommended", + "auditModelHint": "Used only for Audit Agent approvals; manual approval does not call a model.", + "testAuditModel": "Test audit model", + "retentionDays": "Resolved audit log retention days", + "retentionDaysHint": "0 keeps logs forever; blank uses the 90-day default.", + "toolWhitelist": "No-approval tool whitelist", + "toolWhitelistPlaceholder": "read_file, list_dir, glob", + "toolWhitelistHint": "One per line or comma-separated; whitelisted tools skip human-in-the-loop approval.", + "auditAgentTitle": "Audit Agent strategy", + "auditPromptApproval": "Approval-mode prompt", + "auditPromptHint": "Leave blank to use the backend default strategy.", + "auditPromptReviewEdit": "Review-edit-mode prompt", + "auditPromptReviewEditHint": "Review-edit mode can approve with narrowed editedArguments." + }, "knowledge": { "title": "Knowledge base" }, diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 81326e8d..be69f7f2 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -1209,6 +1209,7 @@ "title": "系统设置", "nav": { "basic": "基本设置", + "hitl": "人机协同", "knowledge": "知识库", "c2": "C2", "robots": "机器人设置", @@ -1220,6 +1221,30 @@ "infocollect": { "title": "信息收集" }, + "hitl": { + "title": "人机协同", + "approvalTitle": "审批配置", + "defaultReviewer": "全局默认审批方", + "defaultReviewerHint": "未选会话和新建会话默认使用该审批方;会话侧栏仍可临时覆盖。", + "auditModelTitle": "审计 Agent 模型", + "auditModelReuseMain": "跟随主模型配置", + "auditModelBaseUrlPlaceholder": "留空则复用主模型 Base URL", + "auditModelApiKeyPlaceholder": "留空则复用主模型 API Key", + "auditModelName": "审批模型", + "auditModelNamePlaceholder": "留空则复用主模型;建议填写小模型", + "auditModelHint": "仅审计 Agent 审批时使用;人工审批不消耗模型。", + "testAuditModel": "测试审计模型", + "retentionDays": "已决策审计日志保留天数", + "retentionDaysHint": "0 表示不自动清理;留空使用默认 90 天。", + "toolWhitelist": "免审批工具白名单", + "toolWhitelistPlaceholder": "read_file, list_dir, glob", + "toolWhitelistHint": "每行一个或逗号分隔;白名单工具不会触发人机协同审批。", + "auditAgentTitle": "审计 Agent 策略", + "auditPromptApproval": "审批模式提示词", + "auditPromptHint": "留空时使用后端内置默认策略。", + "auditPromptReviewEdit": "审查编辑模式提示词", + "auditPromptReviewEditHint": "审查编辑模式可通过 editedArguments 收窄参数后放行。" + }, "knowledge": { "title": "知识库设置" }, diff --git a/web/static/js/settings.js b/web/static/js/settings.js index 890d3835..3bbe1998 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -16,6 +16,190 @@ function settingsT(key, fallback) { return fallback; } +const settingsCustomSelects = new Map(); +let settingsCustomSelectsDocBound = false; + +function shouldEnhanceSettingsSelect(select) { + if (!select || select.dataset.settingsCustomSelect === '1') return false; + if (select.classList.contains('model-pick-native')) return false; + if (select.id && select.id.indexOf('audit-filter-') === 0) return false; + if (select.getAttribute('aria-hidden') === 'true') return false; + if (select.style && select.style.display === 'none') return false; + return true; +} + +function closeSettingsCustomSelect(select) { + const reg = settingsCustomSelects.get(select); + if (reg) { + reg.wrapper.classList.remove('open'); + reg.trigger.setAttribute('aria-expanded', 'false'); + } +} + +function closeAllSettingsCustomSelects() { + settingsCustomSelects.forEach((reg) => { + reg.wrapper.classList.remove('open'); + reg.trigger.setAttribute('aria-expanded', 'false'); + }); +} + +function syncSettingsCustomSelect(select) { + const reg = settingsCustomSelects.get(select); + if (!reg) return; + const selected = select.options[select.selectedIndex]; + reg.value.textContent = selected ? selected.textContent : ''; + reg.trigger.disabled = !!select.disabled; + reg.wrapper.classList.toggle('is-disabled', !!select.disabled); + reg.menu.innerHTML = ''; + + Array.prototype.forEach.call(select.options, (option, index) => { + const item = document.createElement('button'); + item.type = 'button'; + item.className = 'settings-custom-select-option'; + item.setAttribute('role', 'option'); + item.setAttribute('data-index', String(index)); + item.setAttribute('aria-selected', option.selected ? 'true' : 'false'); + item.disabled = !!option.disabled; + item.classList.toggle('is-selected', option.selected); + item.classList.toggle('is-disabled', !!option.disabled); + + const check = document.createElement('span'); + check.className = 'settings-custom-select-check'; + check.setAttribute('aria-hidden', 'true'); + check.textContent = '✓'; + + const label = document.createElement('span'); + label.className = 'settings-custom-select-label'; + label.textContent = option.textContent; + + item.appendChild(check); + item.appendChild(label); + reg.menu.appendChild(item); + }); +} + +function refreshSettingsCustomSelects() { + settingsCustomSelects.forEach((_reg, select) => syncSettingsCustomSelect(select)); +} + +function enhanceSettingsSelect(select) { + if (!shouldEnhanceSettingsSelect(select)) { + if (select && select.dataset.settingsCustomSelect === '1') { + syncSettingsCustomSelect(select); + } + return; + } + + select.dataset.settingsCustomSelect = '1'; + select.classList.add('settings-native-select'); + select.tabIndex = -1; + select.setAttribute('aria-hidden', 'true'); + + const wrapper = document.createElement('div'); + wrapper.className = 'settings-custom-select'; + if (select.id && select.id.indexOf('openai-reasoning-') === 0) { + wrapper.classList.add('settings-custom-select--compact'); + } + if (select.style.width) wrapper.style.width = select.style.width; + if (select.style.minWidth) wrapper.style.minWidth = select.style.minWidth; + + const trigger = document.createElement('button'); + trigger.type = 'button'; + trigger.className = 'settings-custom-select-trigger'; + trigger.setAttribute('aria-haspopup', 'listbox'); + trigger.setAttribute('aria-expanded', 'false'); + + const value = document.createElement('span'); + value.className = 'settings-custom-select-value'; + const caret = document.createElement('span'); + caret.className = 'settings-custom-select-caret'; + caret.setAttribute('aria-hidden', 'true'); + caret.textContent = '▾'; + trigger.appendChild(value); + trigger.appendChild(caret); + + const menu = document.createElement('div'); + menu.className = 'settings-custom-select-menu'; + menu.setAttribute('role', 'listbox'); + + const parent = select.parentNode; + parent.insertBefore(wrapper, select); + wrapper.appendChild(trigger); + wrapper.appendChild(menu); + wrapper.appendChild(select); + + settingsCustomSelects.set(select, { wrapper, trigger, value, menu }); + + trigger.addEventListener('click', (event) => { + event.stopPropagation(); + if (select.disabled) return; + const willOpen = !wrapper.classList.contains('open'); + closeAllSettingsCustomSelects(); + wrapper.classList.toggle('open', willOpen); + trigger.setAttribute('aria-expanded', willOpen ? 'true' : 'false'); + }); + + trigger.addEventListener('keydown', (event) => { + if (select.disabled) return; + const enabledOptions = Array.prototype.filter.call(select.options, (option) => !option.disabled); + if (!enabledOptions.length) return; + const current = Math.max(0, enabledOptions.indexOf(select.options[select.selectedIndex])); + let next = current; + if (event.key === 'ArrowDown') next = Math.min(enabledOptions.length - 1, current + 1); + else if (event.key === 'ArrowUp') next = Math.max(0, current - 1); + else if (event.key === 'Home') next = 0; + else if (event.key === 'End') next = enabledOptions.length - 1; + else if (event.key === 'Escape') { + closeSettingsCustomSelect(select); + return; + } else if (event.key === 'Enter' || event.key === ' ') { + wrapper.classList.add('open'); + trigger.setAttribute('aria-expanded', 'true'); + event.preventDefault(); + return; + } else { + return; + } + event.preventDefault(); + const nextOption = enabledOptions[next]; + if (nextOption && select.value !== nextOption.value) { + select.value = nextOption.value; + select.dispatchEvent(new Event('change', { bubbles: true })); + } + syncSettingsCustomSelect(select); + }); + + menu.addEventListener('click', (event) => { + const item = event.target.closest('.settings-custom-select-option'); + if (!item || item.disabled) return; + event.stopPropagation(); + const option = select.options[Number(item.dataset.index)]; + if (option && !option.disabled && select.value !== option.value) { + select.value = option.value; + select.dispatchEvent(new Event('change', { bubbles: true })); + } + syncSettingsCustomSelect(select); + closeSettingsCustomSelect(select); + }); + + select.addEventListener('change', () => syncSettingsCustomSelect(select)); + syncSettingsCustomSelect(select); +} + +function initSettingsCustomSelects(root) { + const scope = root || document.getElementById('page-settings'); + if (!scope) return; + scope.querySelectorAll('select').forEach(enhanceSettingsSelect); + if (!settingsCustomSelectsDocBound) { + document.addEventListener('click', closeAllSettingsCustomSelects); + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape') closeAllSettingsCustomSelects(); + }); + settingsCustomSelectsDocBound = true; + } + refreshSettingsCustomSelects(); +} + function getRobotStatus(type) { const value = (id) => document.getElementById(id)?.value?.trim() || ''; const checked = (id) => document.getElementById(id)?.checked === true; @@ -282,6 +466,7 @@ function syncRobotAgentModeSelectOptions(multiEnabled) { if (!multiEnabled && ['deep', 'plan_execute', 'supervisor'].indexOf(sel.value) >= 0) { sel.value = 'eino_single'; } + syncSettingsCustomSelect(sel); } /** 首次进入仪表盘等页面前拉一次配置,隐藏侧栏 C2(避免禁用后仍显示) */ @@ -340,6 +525,7 @@ function switchSettingsSection(section) { const activeContent = document.getElementById(`settings-section-${section}`); if (activeContent) { activeContent.classList.add('active'); + initSettingsCustomSelects(activeContent); } if (section === 'terminal' && typeof initTerminal === 'function') { setTimeout(initTerminal, 0); @@ -361,6 +547,7 @@ async function openSettings() { // 每次打开时重新加载最新配置(系统设置页面不需要加载工具列表) await loadConfig(false); + initSettingsCustomSelects(); // 清除之前的验证错误状态 document.querySelectorAll('.form-group input').forEach(input => { @@ -459,6 +646,42 @@ async function loadConfig(loadTools = true) { if (fofaEmailEl) fofaEmailEl.value = fofa.email || ''; if (fofaKeyEl) fofaKeyEl.value = fofa.api_key || ''; if (fofaBaseUrlEl) fofaBaseUrlEl.value = fofa.base_url || ''; + + // 填充人机协同配置 + const hitl = currentConfig.hitl || {}; + const hitlReviewerEl = document.getElementById('hitl-default-reviewer'); + if (hitlReviewerEl) { + const reviewer = String(hitl.default_reviewer || 'human').trim().toLowerCase(); + hitlReviewerEl.value = reviewer === 'audit_agent' ? 'audit_agent' : 'human'; + } + const hitlAuditModel = hitl.audit_model || {}; + const hitlAuditProviderEl = document.getElementById('hitl-audit-model-provider'); + if (hitlAuditProviderEl) { + const provider = String(hitlAuditModel.provider || '').trim().toLowerCase(); + hitlAuditProviderEl.value = ['openai', 'claude'].includes(provider) ? provider : ''; + } + const hitlAuditBaseUrlEl = document.getElementById('hitl-audit-model-base-url'); + if (hitlAuditBaseUrlEl) hitlAuditBaseUrlEl.value = hitlAuditModel.base_url || ''; + const hitlAuditApiKeyEl = document.getElementById('hitl-audit-model-api-key'); + if (hitlAuditApiKeyEl) hitlAuditApiKeyEl.value = hitlAuditModel.api_key || ''; + const hitlAuditModelNameEl = document.getElementById('hitl-audit-model-name'); + if (hitlAuditModelNameEl) hitlAuditModelNameEl.value = hitlAuditModel.model || ''; + const hitlRetentionEl = document.getElementById('hitl-retention-days'); + if (hitlRetentionEl) { + hitlRetentionEl.value = (hitl.retention_days === undefined || hitl.retention_days === null) ? '90' : String(hitl.retention_days); + } + const hitlWhitelistEl = document.getElementById('hitl-tool-whitelist'); + if (hitlWhitelistEl) { + hitlWhitelistEl.value = Array.isArray(hitl.tool_whitelist) ? hitl.tool_whitelist.join('\n') : ''; + } + const hitlApprovalPromptEl = document.getElementById('hitl-audit-agent-prompt-settings'); + if (hitlApprovalPromptEl) { + hitlApprovalPromptEl.value = hitl.audit_agent_prompt || ''; + } + const hitlReviewEditPromptEl = document.getElementById('hitl-audit-agent-prompt-review-edit-settings'); + if (hitlReviewEditPromptEl) { + hitlReviewEditPromptEl.value = hitl.audit_agent_prompt_review_edit || ''; + } // 填充Agent配置 document.getElementById('agent-max-iterations').value = currentConfig.agent.max_iterations || 30; @@ -723,6 +946,8 @@ async function loadConfig(loadTools = true) { if (qqSandbox) qqSandbox.checked = qq.sandbox === true; bindRobotManagerEvents(); refreshRobotManager(); + initSettingsCustomSelects(); + refreshSettingsCustomSelects(); // 只有在需要时才加载工具列表(MCP管理页面需要,系统设置页面不需要) if (loadTools) { @@ -1530,6 +1755,15 @@ async function applySettings() { const wecomAgentIdVal = document.getElementById('robot-wecom-agent-id')?.value.trim(); const prevOpenai = (currentConfig && currentConfig.openai) ? currentConfig.openai : {}; const prevRobots = (currentConfig && currentConfig.robots) ? currentConfig.robots : {}; + const prevHitl = (currentConfig && currentConfig.hitl) ? currentConfig.hitl : {}; + const hitlRetentionRaw = document.getElementById('hitl-retention-days')?.value; + const hitlRetention = parseInt(hitlRetentionRaw, 10); + const hitlWhitelistRaw = document.getElementById('hitl-tool-whitelist')?.value || ''; + const hitlToolsSplit = (typeof window.hitlToolsSplitToArray === 'function') + ? window.hitlToolsSplitToArray + : function (s) { + return String(s || '').split(/[\n,,]/).map(v => v.trim()).filter(Boolean); + }; const config = { openai: { ...prevOpenai, @@ -1552,6 +1786,21 @@ async function applySettings() { api_key: document.getElementById('fofa-api-key')?.value.trim() || '', base_url: document.getElementById('fofa-base-url')?.value.trim() || '' }, + hitl: { + ...prevHitl, + audit_model: { + ...(prevHitl.audit_model || {}), + provider: document.getElementById('hitl-audit-model-provider')?.value || '', + base_url: document.getElementById('hitl-audit-model-base-url')?.value.trim() || '', + api_key: document.getElementById('hitl-audit-model-api-key')?.value.trim() || '', + model: document.getElementById('hitl-audit-model-name')?.value.trim() || '' + }, + default_reviewer: document.getElementById('hitl-default-reviewer')?.value === 'audit_agent' ? 'audit_agent' : 'human', + retention_days: Number.isNaN(hitlRetention) ? 90 : Math.max(0, hitlRetention), + tool_whitelist: hitlToolsSplit(hitlWhitelistRaw), + audit_agent_prompt: document.getElementById('hitl-audit-agent-prompt-settings')?.value.trim() || '', + audit_agent_prompt_review_edit: document.getElementById('hitl-audit-agent-prompt-review-edit-settings')?.value.trim() || '' + }, agent: { max_iterations: parseInt(document.getElementById('agent-max-iterations').value) || 30 }, @@ -2012,8 +2261,14 @@ function initModelListControls() { visionProv.dataset.modelListBound = '1'; visionProv.addEventListener('change', syncModelListFetchButtons); } + const hitlAuditProv = document.getElementById('hitl-audit-model-provider'); + if (hitlAuditProv && !hitlAuditProv.dataset.modelListBound) { + hitlAuditProv.dataset.modelListBound = '1'; + hitlAuditProv.addEventListener('change', syncModelListFetchButtons); + } bindModelSelect('openai'); bindModelSelect('vision'); + bindModelSelect('hitlAudit'); syncModelListFetchButtons(); } @@ -2021,6 +2276,9 @@ function modelSelectIds(scope) { if (scope === 'vision') { return { selectId: 'vision-model-select', inputId: 'vision-model' }; } + if (scope === 'hitlAudit') { + return { selectId: 'hitl-audit-model-select', inputId: 'hitl-audit-model-name' }; + } return { selectId: 'openai-model-select', inputId: 'openai-model' }; } @@ -2047,6 +2305,15 @@ function resolveModelListCredentials(scope) { || (document.getElementById('openai-api-key')?.value || '').trim(); return { provider, base_url: baseUrl, api_key: apiKey }; } + if (scope === 'hitlAudit') { + const hp = (document.getElementById('hitl-audit-model-provider')?.value || '').trim(); + const provider = hp || document.getElementById('openai-provider')?.value || 'openai'; + const baseUrl = (document.getElementById('hitl-audit-model-base-url')?.value || '').trim() + || (document.getElementById('openai-base-url')?.value || '').trim(); + const apiKey = (document.getElementById('hitl-audit-model-api-key')?.value || '').trim() + || (document.getElementById('openai-api-key')?.value || '').trim(); + return { provider, base_url: baseUrl, api_key: apiKey }; + } return { provider: document.getElementById('openai-provider')?.value || 'openai', base_url: (document.getElementById('openai-base-url')?.value || '').trim(), @@ -2106,6 +2373,32 @@ function syncModelListFetchButtons() { visionHint.style.display = 'none'; } } + + const hp = (document.getElementById('hitl-audit-model-provider')?.value || '').trim(); + const hitlAuditEffectiveProv = hp || openaiProv; + const hitlAuditBtn = document.getElementById('fetch-hitl-audit-models-btn'); + const hitlAuditHint = document.getElementById('fetch-hitl-audit-models-hint'); + const hitlAuditSelect = document.getElementById('hitl-audit-model-select'); + const isClaudeHitlAudit = hitlAuditEffectiveProv === 'claude'; + if (hitlAuditBtn) { + hitlAuditBtn.style.display = isClaudeHitlAudit ? 'none' : ''; + } + if (hitlAuditSelect && isClaudeHitlAudit) { + hitlAuditSelect.style.display = 'none'; + const hitlAuditWrap = modelPickSelectMap['hitl-audit-model-select']; + if (hitlAuditWrap) hitlAuditWrap.wrapper.style.display = 'none'; + } else if (hitlAuditSelect && !isClaudeHitlAudit) { + syncModelPickDropdown('hitl-audit-model-select'); + } + if (hitlAuditHint) { + if (isClaudeHitlAudit) { + hitlAuditHint.textContent = tFn('settingsBasic.modelsListClaudeHint'); + hitlAuditHint.style.display = ''; + } else { + hitlAuditHint.textContent = ''; + hitlAuditHint.style.display = 'none'; + } + } } function populateModelSelect(scope, models, currentValue) { @@ -2145,9 +2438,13 @@ function populateModelSelect(scope, models, currentValue) { async function fetchModelList(scope) { const tFn = typeof window.t === 'function' ? window.t : (k) => k; const creds = resolveModelListCredentials(scope); - const btnId = scope === 'vision' ? 'fetch-vision-models-btn' : 'fetch-openai-models-btn'; - const resultId = scope === 'vision' ? 'fetch-vision-models-result' : 'fetch-openai-models-result'; - const inputId = scope === 'vision' ? 'vision-model' : 'openai-model'; + const btnId = scope === 'vision' + ? 'fetch-vision-models-btn' + : (scope === 'hitlAudit' ? 'fetch-hitl-audit-models-btn' : 'fetch-openai-models-btn'); + const resultId = scope === 'vision' + ? 'fetch-vision-models-result' + : (scope === 'hitlAudit' ? 'fetch-hitl-audit-models-result' : 'fetch-openai-models-result'); + const inputId = modelSelectIds(scope).inputId; const btn = document.getElementById(btnId); const resultEl = document.getElementById(resultId); const inputEl = document.getElementById(inputId); @@ -2258,6 +2555,75 @@ async function testVisionConnection() { } } +function collectHitlAuditModelEffectiveConfig() { + const main = { + provider: document.getElementById('openai-provider')?.value || 'openai', + api_key: document.getElementById('openai-api-key')?.value.trim() || '', + base_url: document.getElementById('openai-base-url')?.value.trim() || '', + model: document.getElementById('openai-model')?.value.trim() || '' + }; + return { + provider: document.getElementById('hitl-audit-model-provider')?.value || main.provider, + base_url: document.getElementById('hitl-audit-model-base-url')?.value.trim() || main.base_url, + api_key: document.getElementById('hitl-audit-model-api-key')?.value.trim() || main.api_key, + model: document.getElementById('hitl-audit-model-name')?.value.trim() || main.model + }; +} + +async function testHitlAuditModelConnection() { + const btn = document.getElementById('test-hitl-audit-model-btn'); + const resultEl = document.getElementById('test-hitl-audit-model-result'); + const cfg = collectHitlAuditModelEffectiveConfig(); + + if (!cfg.api_key || !cfg.model) { + if (resultEl) { + resultEl.style.color = 'var(--danger-color, #e53e3e)'; + resultEl.textContent = typeof window.t === 'function' ? window.t('settingsBasic.testFillRequired') : '请先填写 API Key 和模型'; + } + return; + } + + if (btn) { + btn.style.pointerEvents = 'none'; + btn.style.opacity = '0.5'; + } + if (resultEl) { + resultEl.style.color = 'var(--text-muted, #888)'; + resultEl.textContent = typeof window.t === 'function' ? window.t('settingsBasic.testing') : '测试中...'; + } + + try { + const response = await apiFetch('/api/config/test-openai', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(cfg) + }); + const result = await response.json(); + + if (result.success) { + if (resultEl) { + resultEl.style.color = 'var(--success-color, #38a169)'; + const latency = result.latency_ms ? ` (${result.latency_ms}ms)` : ''; + const modelInfo = result.model ? ` [${result.model}]` : ''; + resultEl.textContent = (typeof window.t === 'function' ? window.t('settingsBasic.testSuccess') : '连接成功') + modelInfo + latency; + } + } else if (resultEl) { + resultEl.style.color = 'var(--danger-color, #e53e3e)'; + resultEl.textContent = (typeof window.t === 'function' ? window.t('settingsBasic.testFailed') : '连接失败') + ': ' + (result.error || '未知错误'); + } + } catch (error) { + if (resultEl) { + resultEl.style.color = 'var(--danger-color, #e53e3e)'; + resultEl.textContent = (typeof window.t === 'function' ? window.t('settingsBasic.testError') : '测试出错') + ': ' + error.message; + } + } finally { + if (btn) { + btn.style.pointerEvents = ''; + btn.style.opacity = ''; + } + } +} + // 测试OpenAI连接 async function testOpenAIConnection() { const btn = document.getElementById('test-openai-btn'); @@ -3172,6 +3538,11 @@ openSettings = async function() { // 语言切换后重新渲染 MCP 管理页中由 JS 写入的区块(innerHTML 不会随 data-i18n 自动更新) document.addEventListener('languagechange', function () { try { + const settingsPage = document.getElementById('page-settings'); + if (settingsPage) { + initSettingsCustomSelects(settingsPage); + refreshSettingsCustomSelects(); + } const mcpPage = document.getElementById('page-mcp-management'); if (mcpPage && mcpPage.classList.contains('active')) { if (typeof loadExternalMCPs === 'function') { diff --git a/web/templates/index.html b/web/templates/index.html index 996f188e..9c2651b3 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -2818,6 +2818,9 @@
基本设置
+
+ 人机协同 +
信息收集
@@ -3052,6 +3055,93 @@ + +
+
+

人机协同

+
+ +
+

审批配置

+
+
+ + + 未选会话和新建会话默认使用该审批方;会话侧栏仍可临时覆盖。 +
+
+
审计 Agent 模型
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + 获取列表 +
+ + + 仅审计 Agent 审批时使用;人工审批不消耗模型。 +
+
+ 测试审计模型 + +
+
+ + + 0 表示不自动清理;留空使用默认 90 天。 +
+
+ + + 每行一个或逗号分隔;白名单工具不会触发人机协同审批。 +
+
+
+ +
+

审计 Agent 策略

+
+
+ + + 留空时使用后端内置默认策略。 +
+
+ + + 审查编辑模式可通过 editedArguments 收窄参数后放行。 +
+
+
+ +
+ +
+
+