Add files via upload

This commit is contained in:
公明
2026-07-07 11:31:38 +08:00
committed by GitHub
parent 6dffa64513
commit 84cbaa57dc
5 changed files with 721 additions and 3 deletions
+207
View File
@@ -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;
+25
View File
@@ -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"
},
+25
View File
@@ -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": "知识库设置"
},
+374 -3
View File
@@ -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') {
+90
View File
@@ -2818,6 +2818,9 @@
<div class="settings-nav-item active" data-section="basic" onclick="switchSettingsSection('basic')">
<span data-i18n="settings.nav.basic">基本设置</span>
</div>
<div class="settings-nav-item" data-section="hitl" onclick="switchSettingsSection('hitl')">
<span data-i18n="settings.nav.hitl">人机协同</span>
</div>
<div class="settings-nav-item" data-section="infocollect" onclick="switchSettingsSection('infocollect')">
<span data-i18n="settings.nav.infocollect">信息收集</span>
</div>
@@ -3052,6 +3055,93 @@
</div>
</div>
<!-- 人机协同 -->
<div id="settings-section-hitl" class="settings-section-content">
<div class="settings-section-header">
<h3 data-i18n="settings.hitl.title">人机协同</h3>
</div>
<div class="settings-subsection">
<h4 data-i18n="settings.hitl.approvalTitle">审批配置</h4>
<div class="settings-form">
<div class="form-group">
<label for="hitl-default-reviewer" data-i18n="settings.hitl.defaultReviewer">全局默认审批方</label>
<select id="hitl-default-reviewer" class="form-select">
<option value="human" data-i18n="chat.hitlReviewerHuman">人工审批</option>
<option value="audit_agent" data-i18n="chat.hitlReviewerAgent">审计 Agent</option>
</select>
<small class="form-hint" data-i18n="settings.hitl.defaultReviewerHint">未选会话和新建会话默认使用该审批方;会话侧栏仍可临时覆盖。</small>
</div>
<div class="settings-subsection-header">
<h5 data-i18n="settings.hitl.auditModelTitle">审计 Agent 模型</h5>
</div>
<div class="form-group">
<label for="hitl-audit-model-provider" data-i18n="settingsBasic.apiProvider">API 提供商</label>
<select id="hitl-audit-model-provider" class="form-select">
<option value="" data-i18n="settings.hitl.auditModelReuseMain">跟随主模型配置</option>
<option value="openai" data-i18n="settingsBasic.providerOpenAI">OpenAI / 兼容 OpenAI 协议</option>
<option value="claude" data-i18n="settingsBasic.providerClaude">Claude (Anthropic Messages API)</option>
</select>
</div>
<div class="form-group">
<label for="hitl-audit-model-base-url">Base URL</label>
<input type="text" id="hitl-audit-model-base-url" data-i18n="settings.hitl.auditModelBaseUrlPlaceholder" data-i18n-attr="placeholder" placeholder="留空则复用主模型 Base URL" />
</div>
<div class="form-group">
<label for="hitl-audit-model-api-key">API Key</label>
<input type="password" id="hitl-audit-model-api-key" data-i18n="settings.hitl.auditModelApiKeyPlaceholder" data-i18n-attr="placeholder" placeholder="留空则复用主模型 API Key" autocomplete="off" />
</div>
<div class="form-group">
<label for="hitl-audit-model-name" data-i18n="settings.hitl.auditModelName">审批模型</label>
<div class="model-pick-row">
<input type="text" id="hitl-audit-model-name" class="model-pick-input" data-i18n="settings.hitl.auditModelNamePlaceholder" data-i18n-attr="placeholder" placeholder="留空则复用主模型;建议填写小模型" />
<select id="hitl-audit-model-select" class="model-pick-native" style="display: none;" aria-hidden="true" tabindex="-1">
<option value="" disabled data-i18n="settingsBasic.modelsListSelectPlaceholder">请选择模型</option>
</select>
<a href="javascript:void(0)" id="fetch-hitl-audit-models-btn" class="model-pick-fetch-link" onclick="fetchModelList('hitlAudit')" data-i18n="settingsBasic.fetchModels">获取列表</a>
</div>
<small id="fetch-hitl-audit-models-hint" class="form-hint" style="display: none; font-size: 0.75rem; margin-top: 4px;"></small>
<span id="fetch-hitl-audit-models-result" style="font-size: 0.75rem; margin-top: 2px; display: block;"></span>
<small class="form-hint" data-i18n="settings.hitl.auditModelHint">仅审计 Agent 审批时使用;人工审批不消耗模型。</small>
</div>
<div style="display: flex; align-items: center; gap: 8px; margin-top: 2px;">
<a href="javascript:void(0)" id="test-hitl-audit-model-btn" onclick="testHitlAuditModelConnection()" style="font-size: 0.8125rem; color: var(--accent-color, #3182ce); text-decoration: none; cursor: pointer; user-select: none;" data-i18n="settings.hitl.testAuditModel">测试审计模型</a>
<span id="test-hitl-audit-model-result" style="font-size: 0.8125rem;"></span>
</div>
<div class="form-group">
<label for="hitl-retention-days" data-i18n="settings.hitl.retentionDays">已决策审计日志保留天数</label>
<input type="number" id="hitl-retention-days" min="0" step="1" placeholder="90" />
<small class="form-hint" data-i18n="settings.hitl.retentionDaysHint">0 表示不自动清理;留空使用默认 90 天。</small>
</div>
<div class="form-group">
<label for="hitl-tool-whitelist" data-i18n="settings.hitl.toolWhitelist">免审批工具白名单</label>
<textarea id="hitl-tool-whitelist" rows="4" spellcheck="false" autocomplete="off" data-i18n="settings.hitl.toolWhitelistPlaceholder" data-i18n-attr="placeholder" placeholder="read_file, list_dir, glob"></textarea>
<small class="form-hint" data-i18n="settings.hitl.toolWhitelistHint">每行一个或逗号分隔;白名单工具不会触发人机协同审批。</small>
</div>
</div>
</div>
<div class="settings-subsection">
<h4 data-i18n="settings.hitl.auditAgentTitle">审计 Agent 策略</h4>
<div class="settings-form">
<div class="form-group">
<label for="hitl-audit-agent-prompt-settings" data-i18n="settings.hitl.auditPromptApproval">审批模式提示词</label>
<textarea id="hitl-audit-agent-prompt-settings" rows="10" spellcheck="false" autocomplete="off"></textarea>
<small class="form-hint" data-i18n="settings.hitl.auditPromptHint">留空时使用后端内置默认策略。</small>
</div>
<div class="form-group">
<label for="hitl-audit-agent-prompt-review-edit-settings" data-i18n="settings.hitl.auditPromptReviewEdit">审查编辑模式提示词</label>
<textarea id="hitl-audit-agent-prompt-review-edit-settings" rows="10" spellcheck="false" autocomplete="off"></textarea>
<small class="form-hint" data-i18n="settings.hitl.auditPromptReviewEditHint">审查编辑模式可通过 editedArguments 收窄参数后放行。</small>
</div>
</div>
</div>
<div class="settings-actions">
<button type="button" class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
</div>
</div>
<!-- 信息收集设置 -->
<div id="settings-section-infocollect" class="settings-section-content">
<div class="settings-section-header">