mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-09-30 05:02:06 +02:00
feat: add TypeSafe Jev as HITL audit backend
Let audit_agent approve or reject with one System One call instead of chat JSON, while keeping the OpenAI-compatible backend as an option. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2479,6 +2479,19 @@ html[data-theme="dark"] .sidebar-content:hover::-webkit-scrollbar-thumb:hover {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.hitl-page-audit-engine {
|
||||
margin: 10px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.hitl-log-engine {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.hitl-page-reviewer-hint {
|
||||
margin: 10px 0 0;
|
||||
font-size: 12px;
|
||||
@@ -36978,6 +36991,7 @@ html[data-theme="dark"] .info-collect-cell-modal-title {
|
||||
-webkit-text-fill-color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .hitl-page-audit-engine,
|
||||
html[data-theme="dark"] .hitl-page-reviewer-hint,
|
||||
html[data-theme="dark"] .hitl-page-whitelist-hint,
|
||||
html[data-theme="dark"] .hitl-page-strategy-hint,
|
||||
|
||||
@@ -917,6 +917,10 @@
|
||||
"hitl": {
|
||||
"pageTitle": "HITL approvals",
|
||||
"pageReviewerLabel": "Current reviewer",
|
||||
"auditEngineLabel": "Approval engine",
|
||||
"auditEngineOpenAI": "OpenAI protocol",
|
||||
"auditEngineJev": "TypeSafe Jev",
|
||||
"colAuditEngine": "Approval engine",
|
||||
"pageReviewerHint": "Applies to the selected conversation. Without a conversation, saved to config.yaml as the global default for new chats. Takes effect immediately.",
|
||||
"pageReviewerSaved": "Reviewer saved.",
|
||||
"whitelistLabel": "Tool whitelist (no approval)",
|
||||
@@ -928,6 +932,7 @@
|
||||
"strategyTabApproval": "Approval mode",
|
||||
"strategyTabReviewEdit": "Review & edit mode",
|
||||
"strategyHintApproval": "Whitelisted tools skip approval. In approval mode the Audit Agent only approves or rejects.",
|
||||
"strategyHintJev": "TypeSafe Jev is active: custom strategy text below is evaluated as structured questions. Built-in destructive rules remain a hard floor. Jev cannot rewrite arguments.",
|
||||
"strategyHintReviewEdit": "In review & edit mode the Audit Agent may narrow parameters via editedArguments before approve; reject if parameters cannot be safely adjusted.",
|
||||
"strategyReset": "Reset to default",
|
||||
"strategySaved": "Audit strategy saved.",
|
||||
@@ -1790,13 +1795,22 @@
|
||||
"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",
|
||||
"auditBackend": "Approval engine",
|
||||
"auditBackendHint": "Choose one: an OpenAI-compatible chat model returns JSON from the prompt, or TypeSafe Jev makes a structured allow/block decision (no argument editing).",
|
||||
"auditBackendOpenAI": "OpenAI-compatible model",
|
||||
"auditBackendTypeSafe": "TypeSafe Jev",
|
||||
"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.",
|
||||
"auditModelTypeSafeHint": "Jev uses TypeSafe System One, not the OpenAI protocol. The API key is required and is not reused from the main model. Leave the model blank to use jev-latest. In review-edit mode Jev only allows or blocks; it will not rewrite arguments.",
|
||||
"auditModelTypeSafeBaseUrlPlaceholder": "Leave blank to use https://api.typesafe.ai",
|
||||
"auditModelTypeSafeApiKeyPlaceholder": "TypeSafe API key (required; not reused from the main model)",
|
||||
"auditModelTypeSafeNamePlaceholder": "Leave blank to use jev-latest",
|
||||
"testAuditModel": "Test audit model",
|
||||
"testTypeSafeFillRequired": "Enter a TypeSafe API key first",
|
||||
"retentionDays": "Resolved audit log retention days",
|
||||
"retentionDaysHint": "0 keeps logs forever; blank uses the 90-day default.",
|
||||
"toolWhitelist": "No-approval tool whitelist",
|
||||
@@ -1805,6 +1819,7 @@
|
||||
"auditAgentTitle": "Audit Agent strategy",
|
||||
"auditPromptApproval": "Approval-mode prompt",
|
||||
"auditPromptHint": "Leave blank to use the backend default strategy.",
|
||||
"auditPromptTypeSafeHint": "The current engine is TypeSafe Jev: custom strategy text is evaluated as structured questions. Built-in destructive rules remain a hard floor. Jev cannot rewrite arguments.",
|
||||
"auditPromptReviewEdit": "Review-edit-mode prompt",
|
||||
"auditPromptReviewEditHint": "Review-edit mode can approve with narrowed editedArguments."
|
||||
},
|
||||
|
||||
@@ -906,6 +906,10 @@
|
||||
"pageTitle": "人机协同审批",
|
||||
"pageReviewerLabel": "当前审批方",
|
||||
"pageReviewerHint": "作用于当前选中会话;未选会话时写入 config.yaml 作为全局默认,新建会话时沿用。切换后立即生效。",
|
||||
"auditEngineLabel": "审批引擎",
|
||||
"auditEngineOpenAI": "OpenAI 协议",
|
||||
"auditEngineJev": "TypeSafe Jev",
|
||||
"colAuditEngine": "审批引擎",
|
||||
"pageReviewerSaved": "审批方已保存。",
|
||||
"whitelistLabel": "免审批工具白名单",
|
||||
"whitelistHint": "每行一个或逗号分隔;保存后写入 config.yaml 全局白名单并立即生效(与聊天侧栏同步展示)。",
|
||||
@@ -916,6 +920,7 @@
|
||||
"strategyTabApproval": "审批模式",
|
||||
"strategyTabReviewEdit": "审查编辑模式",
|
||||
"strategyHintApproval": "白名单内工具免审批;审批模式下审计 Agent 仅裁决通过/拒绝。",
|
||||
"strategyHintJev": "当前是 TypeSafe Jev:会读取下面的自定义策略并编成结构化问题;破坏业务可用性等内置规则仍是硬底线。Jev 不能改参。",
|
||||
"strategyHintReviewEdit": "审查编辑模式下审计 Agent 可通过 editedArguments 收窄参数后放行;无法安全改参时应拒绝。",
|
||||
"strategyReset": "恢复默认",
|
||||
"strategySaved": "审计策略已保存。",
|
||||
@@ -1778,13 +1783,22 @@
|
||||
"defaultReviewer": "全局默认审批方",
|
||||
"defaultReviewerHint": "未选会话和新建会话默认使用该审批方;会话侧栏仍可临时覆盖。",
|
||||
"auditModelTitle": "审计 Agent 模型",
|
||||
"auditBackend": "审批引擎",
|
||||
"auditBackendHint": "二选一:OpenAI 兼容聊天模型按提示词输出 JSON;Jev 用结构化问题做放通/拦截,不能改参。",
|
||||
"auditBackendOpenAI": "OpenAI 协议模型",
|
||||
"auditBackendTypeSafe": "TypeSafe Jev",
|
||||
"auditModelReuseMain": "跟随主模型配置",
|
||||
"auditModelBaseUrlPlaceholder": "留空则复用主模型 Base URL",
|
||||
"auditModelApiKeyPlaceholder": "留空则复用主模型 API Key",
|
||||
"auditModelName": "审批模型",
|
||||
"auditModelNamePlaceholder": "留空则复用主模型;建议填写小模型",
|
||||
"auditModelHint": "仅审计 Agent 审批时使用;人工审批不消耗模型。",
|
||||
"auditModelTypeSafeHint": "Jev 走 TypeSafe System One,不是 OpenAI 协议。API Key 必填,不复用主模型密钥;模型留空使用 jev-latest。审查编辑模式下 Jev 只做放通/拦截,不会改参。",
|
||||
"auditModelTypeSafeBaseUrlPlaceholder": "留空使用 https://api.typesafe.ai",
|
||||
"auditModelTypeSafeApiKeyPlaceholder": "TypeSafe API Key(必填,不复用主模型)",
|
||||
"auditModelTypeSafeNamePlaceholder": "留空使用 jev-latest",
|
||||
"testAuditModel": "测试审计模型",
|
||||
"testTypeSafeFillRequired": "请先填写 TypeSafe API Key",
|
||||
"retentionDays": "已决策审计日志保留天数",
|
||||
"retentionDaysHint": "0 表示不自动清理;留空使用默认 90 天。",
|
||||
"toolWhitelist": "免审批工具白名单",
|
||||
@@ -1793,6 +1807,7 @@
|
||||
"auditAgentTitle": "审计 Agent 策略",
|
||||
"auditPromptApproval": "审批模式提示词",
|
||||
"auditPromptHint": "留空时使用后端内置默认策略。",
|
||||
"auditPromptTypeSafeHint": "当前审批引擎是 TypeSafe Jev:会读取本页/人机协同页的自定义策略并编成结构化问题;内置破坏性规则仍是硬底线。Jev 不能改参。",
|
||||
"auditPromptReviewEdit": "审查编辑模式提示词",
|
||||
"auditPromptReviewEditHint": "审查编辑模式可通过 editedArguments 收窄参数后放行。"
|
||||
},
|
||||
|
||||
+31
-1
@@ -143,6 +143,7 @@ let chatAIChannels = {};
|
||||
let chatDefaultAIChannel = '';
|
||||
let chatAIChannelIdByNormalizedId = {};
|
||||
let chatHitlAuditModelName = '';
|
||||
let chatHitlAuditBackend = '';
|
||||
let chatSystemModelRequestSeq = 0;
|
||||
let chatSystemModelSaving = false;
|
||||
let chatSystemModelCloseTimer = null;
|
||||
@@ -1070,10 +1071,26 @@ function currentSystemModelLabel() {
|
||||
return model || (ch && (ch.name || chatDefaultAIChannel)) || currentChatModelLabel();
|
||||
}
|
||||
|
||||
function currentHitlAuditBackend() {
|
||||
const b = String(chatHitlAuditBackend || (typeof window !== 'undefined' && window.csaiHitlAuditBackend) || '').trim().toLowerCase();
|
||||
return (b === 'typesafe' || b === 'jev' || b === 'type-safe') ? 'typesafe' : 'openai';
|
||||
}
|
||||
|
||||
function currentHitlAuditModelLabel() {
|
||||
if (currentHitlAuditBackend() === 'typesafe') {
|
||||
return chatHitlAuditModelName || 'jev-latest';
|
||||
}
|
||||
return chatHitlAuditModelName || currentSystemModelLabel();
|
||||
}
|
||||
|
||||
function currentHitlAuditEngineLabel() {
|
||||
const engine = currentHitlAuditBackend() === 'typesafe'
|
||||
? chatTranslate('settings.hitl.auditBackendTypeSafe', 'TypeSafe Jev')
|
||||
: chatTranslate('settings.hitl.auditBackendOpenAI', 'OpenAI 协议模型');
|
||||
const model = currentHitlAuditModelLabel();
|
||||
return engine + (model ? ' · ' + model : '');
|
||||
}
|
||||
|
||||
function resolveChatPickerChannelId() {
|
||||
return selectedChatAIChannelId() || chatDefaultAIChannel;
|
||||
}
|
||||
@@ -1709,7 +1726,7 @@ function updateChatComposerSessionShortcuts(summary) {
|
||||
? chatTranslate('chat.sessionShortcutAuditAgent', 'Agent 审查')
|
||||
: chatTranslate('chat.sessionShortcutHuman', '人工审批');
|
||||
const modeLabel = data.hitl || getHitlModeLabel(cfg.mode);
|
||||
const approvalModel = auditAgent ? currentHitlAuditModelLabel() : '';
|
||||
const approvalModel = auditAgent ? currentHitlAuditEngineLabel() : '';
|
||||
const label = prefix + ':' + modeLabel + (approvalModel ? ' · ' + approvalModel : '');
|
||||
hitlEl.textContent = label;
|
||||
hitlEl.title = label;
|
||||
@@ -2070,9 +2087,22 @@ async function initChatAgentModeFromConfig() {
|
||||
multiAgentAPIEnabled = !!(cfg.multi_agent && cfg.multi_agent.enabled);
|
||||
populateChatAIChannelSelect(cfg.ai || {});
|
||||
const hitlAuditModel = cfg.hitl && cfg.hitl.audit_model;
|
||||
chatHitlAuditBackend = cfg.hitl && typeof cfg.hitl.audit_backend === 'string'
|
||||
? cfg.hitl.audit_backend.trim().toLowerCase()
|
||||
: '';
|
||||
chatHitlAuditModelName = hitlAuditModel && typeof hitlAuditModel.model === 'string'
|
||||
? hitlAuditModel.model.trim()
|
||||
: '';
|
||||
if (typeof window !== 'undefined') {
|
||||
window.csaiHitlAuditBackend = chatHitlAuditBackend;
|
||||
window.csaiHitlAuditModel = chatHitlAuditModelName;
|
||||
if (typeof window.renderHitlPageAuditEngine === 'function') {
|
||||
window.renderHitlPageAuditEngine();
|
||||
}
|
||||
if (typeof window.renderHitlStrategyJevHint === 'function') {
|
||||
window.renderHitlStrategyJevHint();
|
||||
}
|
||||
}
|
||||
updateChatReasoningSummary();
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__csaiMultiAgentPublic = cfg.multi_agent || null;
|
||||
|
||||
@@ -57,7 +57,7 @@ test('输入框可按会话通道获取模型并双向同步会话推理且审
|
||||
assert.match(chat, /function currentHitlAuditModelLabel\(\)/);
|
||||
assert.match(chat, /const label = currentChatModelLabel\(\)/);
|
||||
assert.doesNotMatch(chat, /const label = data\.model \|\| currentChatModelLabel\(\)/);
|
||||
assert.match(chat, /const approvalModel = auditAgent \? currentHitlAuditModelLabel\(\) : ''/);
|
||||
assert.match(chat, /const approvalModel = auditAgent \? currentHitlAuditEngineLabel\(\) : ''/);
|
||||
assert.match(chat, /hitlAuditModel\.model\.trim\(\)/);
|
||||
assert.match(template, /id="chat-model-shortcut"[^>]+onclick="openChatSystemModelPicker\(event\)"/);
|
||||
assert.match(template, /id="chat-system-model-menu"[^>]+hidden/);
|
||||
@@ -361,6 +361,24 @@ test('旧会话首次升级到五分钟默认审批时限,仍允许用户之
|
||||
assert.match(fs.readFileSync('web/static/js/hitl.js', 'utf8'), /markLegacyHitlTimeoutMigrated/);
|
||||
});
|
||||
|
||||
test('人机协同页和日志展示 Jev / OpenAI 审批引擎', () => {
|
||||
const hitlPage = fs.readFileSync('web/static/js/hitl.js', 'utf8');
|
||||
assert.match(template, /id="hitl-page-audit-engine"/);
|
||||
assert.match(template, /id="hitl-log-detail-engine"/);
|
||||
assert.match(hitlPage, /function hitlAuditEngineFromItem/);
|
||||
assert.match(hitlPage, /function renderHitlPageAuditEngine/);
|
||||
assert.match(hitlPage, /function renderHitlStrategyJevHint/);
|
||||
assert.match(template, /id="hitl-strategy-hint-jev"/);
|
||||
assert.equal(zh.hitl.strategyHintJev.includes('Jev'), true);
|
||||
assert.equal(en.hitl.strategyHintJev.includes('Jev'), true);
|
||||
assert.match(handler, /auditBackend/);
|
||||
assert.match(chat, /function currentHitlAuditEngineLabel\(\)/);
|
||||
assert.equal(zh.hitl.auditEngineJev, 'TypeSafe Jev');
|
||||
assert.equal(en.hitl.auditEngineJev, 'TypeSafe Jev');
|
||||
assert.equal(zh.hitl.auditEngineOpenAI, 'OpenAI 协议');
|
||||
assert.equal(en.hitl.auditEngineOpenAI, 'OpenAI protocol');
|
||||
});
|
||||
|
||||
test('审批体验文案具有完整中英文资源', () => {
|
||||
const hitlKeys = [
|
||||
'waitingApprovalShort',
|
||||
|
||||
+139
-2
@@ -272,9 +272,15 @@ function applyHitlDefaultConfigFromServer(data) {
|
||||
reviewer: reviewer,
|
||||
timeoutSeconds: timeoutSeconds
|
||||
};
|
||||
const backend = hitlNormalizeAuditBackend(src.auditBackend || src.audit_backend);
|
||||
const model = String(src.auditModel || src.audit_model || '').trim();
|
||||
if (backend) out.auditBackend = backend;
|
||||
if (model) out.auditModel = model;
|
||||
if (typeof window !== 'undefined') {
|
||||
window.csaiHitlDefaultConfig = out;
|
||||
window.csaiHitlDefaultReviewer = reviewer;
|
||||
if (backend) window.csaiHitlAuditBackend = backend;
|
||||
if (model || backend) window.csaiHitlAuditModel = model;
|
||||
if (Array.isArray(src.hitlGlobalToolWhitelist)) {
|
||||
window.csaiHitlGlobalToolWhitelist = src.hitlGlobalToolWhitelist;
|
||||
}
|
||||
@@ -1088,6 +1094,8 @@ function refreshHitlPageReviewerBar() {
|
||||
if (typeof window.bindHitlReviewerToggleListeners === 'function') {
|
||||
window.bindHitlReviewerToggleListeners();
|
||||
}
|
||||
renderHitlPageAuditEngine();
|
||||
renderHitlStrategyJevHint();
|
||||
}
|
||||
|
||||
let hitlDefaultAuditPrompt = '';
|
||||
@@ -1114,6 +1122,7 @@ function switchHitlStrategyMode(mode) {
|
||||
if (reviewTa) reviewTa.hidden = hitlStrategyMode !== 'review_edit';
|
||||
if (hintApproval) hintApproval.hidden = hitlStrategyMode !== 'approval';
|
||||
if (hintReview) hintReview.hidden = hitlStrategyMode !== 'review_edit';
|
||||
renderHitlStrategyJevHint();
|
||||
}
|
||||
|
||||
function showHitlStrategyFeedback(text, isError) {
|
||||
@@ -1151,6 +1160,23 @@ async function refreshHitlAuditStrategy() {
|
||||
}
|
||||
}
|
||||
|
||||
function renderHitlStrategyJevHint() {
|
||||
let el = document.getElementById('hitl-strategy-hint-jev');
|
||||
const bar = document.querySelector('.hitl-page-strategy-bar') || document.getElementById('hitl-page-strategy-bar');
|
||||
if (!el && bar) {
|
||||
el = document.createElement('p');
|
||||
el.className = 'hitl-page-strategy-hint';
|
||||
el.id = 'hitl-strategy-hint-jev';
|
||||
const reviewHint = document.getElementById('hitl-strategy-hint-review-edit');
|
||||
if (reviewHint && reviewHint.parentNode) reviewHint.parentNode.insertBefore(el, reviewHint.nextSibling);
|
||||
else bar.appendChild(el);
|
||||
}
|
||||
if (!el) return;
|
||||
const ts = hitlCurrentAuditEngine().backend === 'typesafe';
|
||||
el.hidden = !ts;
|
||||
if (ts) el.textContent = hitlT('strategyHintJev', 'TypeSafe Jev evaluates the custom strategy as structured questions. Built-in destructive rules remain a hard floor.');
|
||||
}
|
||||
|
||||
async function saveHitlAuditStrategy() {
|
||||
const approvalTa = document.getElementById('hitl-audit-agent-prompt');
|
||||
const reviewTa = document.getElementById('hitl-audit-agent-prompt-review-edit');
|
||||
@@ -1205,7 +1231,6 @@ function refreshHitlActivePanel() {
|
||||
}
|
||||
|
||||
function hitlDecidedByLabel(v) {
|
||||
const key = 'reviewer' + String(v || 'human').replace(/_([a-z])/g, function (_, c) { return c.toUpperCase(); }).replace(/^./, function (c) { return c.toUpperCase(); });
|
||||
const map = {
|
||||
human: hitlT('reviewerHuman', 'Human'),
|
||||
audit_agent: hitlT('reviewerAgent', 'Audit Agent'),
|
||||
@@ -1215,6 +1240,83 @@ function hitlDecidedByLabel(v) {
|
||||
return map[v] || v || '-';
|
||||
}
|
||||
|
||||
function hitlNormalizeAuditBackend(v) {
|
||||
const s = String(v || '').trim().toLowerCase();
|
||||
if (s === 'typesafe' || s === 'jev' || s === 'type-safe' || s === 'typesafe-ai') return 'typesafe';
|
||||
if (s === 'openai' || s === 'openai_compatible' || s === 'llm') return 'openai';
|
||||
return '';
|
||||
}
|
||||
|
||||
function hitlCurrentAuditEngine() {
|
||||
const cfg = (typeof window !== 'undefined' && window.csaiHitlDefaultConfig) || {};
|
||||
const backend = hitlNormalizeAuditBackend(cfg.auditBackend || (typeof window !== 'undefined' && window.csaiHitlAuditBackend));
|
||||
let model = String(cfg.auditModel || (typeof window !== 'undefined' && window.csaiHitlAuditModel) || '').trim();
|
||||
if (backend === 'typesafe' && !model) model = 'jev-latest';
|
||||
return { backend: backend || 'openai', model: model };
|
||||
}
|
||||
|
||||
function hitlAuditEngineLabel(backend, model) {
|
||||
const b = hitlNormalizeAuditBackend(backend);
|
||||
if (!b) return '';
|
||||
const name = b === 'typesafe'
|
||||
? hitlT('auditEngineJev', 'TypeSafe Jev')
|
||||
: hitlT('auditEngineOpenAI', 'OpenAI protocol');
|
||||
const m = String(model || '').trim();
|
||||
return m ? (name + ' · ' + m) : name;
|
||||
}
|
||||
|
||||
function hitlAuditEngineFromItem(item) {
|
||||
const data = item && typeof item === 'object' ? item : {};
|
||||
let backend = hitlNormalizeAuditBackend(data.auditBackend || data.audit_backend);
|
||||
let model = String(data.auditModel || data.audit_model || '').trim();
|
||||
if (!backend) {
|
||||
const payload = typeof window.hitlParsePayloadObject === 'function'
|
||||
? hitlParsePayloadObject(data.payload || '')
|
||||
: {};
|
||||
const approval = payload && payload.hitlApproval && typeof payload.hitlApproval === 'object'
|
||||
? payload.hitlApproval
|
||||
: {};
|
||||
backend = hitlNormalizeAuditBackend(approval.auditBackend || approval.audit_backend);
|
||||
if (!model) model = String(approval.auditModel || approval.audit_model || '').trim();
|
||||
}
|
||||
if (!backend) {
|
||||
const comment = String(data.comment || '');
|
||||
if (/TypeSafe|破坏分|choice=|Jev/i.test(comment)) backend = 'typesafe';
|
||||
else if (hitlReviewerNormalize(data.decidedBy || data.decided_by) === 'audit_agent') backend = 'openai';
|
||||
}
|
||||
if (backend === 'typesafe' && !model) model = 'jev-latest';
|
||||
return { backend: backend, model: model };
|
||||
}
|
||||
|
||||
function ensureHitlPageAuditEngineEl() {
|
||||
let el = document.getElementById('hitl-page-audit-engine');
|
||||
if (el) return el;
|
||||
const bar = document.getElementById('hitl-page-reviewer-bar');
|
||||
if (!bar) return null;
|
||||
el = document.createElement('p');
|
||||
el.className = 'hitl-page-audit-engine';
|
||||
el.id = 'hitl-page-audit-engine';
|
||||
el.hidden = true;
|
||||
const hint = bar.querySelector('.hitl-page-reviewer-hint');
|
||||
if (hint) bar.insertBefore(el, hint);
|
||||
else bar.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
function renderHitlPageAuditEngine() {
|
||||
const el = ensureHitlPageAuditEngineEl();
|
||||
if (!el) return;
|
||||
const info = hitlCurrentAuditEngine();
|
||||
const engine = hitlAuditEngineLabel(info.backend, info.model);
|
||||
if (!engine) {
|
||||
el.hidden = true;
|
||||
el.textContent = '';
|
||||
return;
|
||||
}
|
||||
el.hidden = false;
|
||||
el.textContent = hitlT('auditEngineLabel', 'Approval engine') + ':' + engine;
|
||||
}
|
||||
|
||||
function hitlFormatTime(v) {
|
||||
if (!v) return '-';
|
||||
try {
|
||||
@@ -1594,7 +1696,11 @@ function renderHitlLogsTable(items) {
|
||||
'<td>' + escapeHtml(String(item.toolName || '-')) + '</td>' +
|
||||
'<td class="hitl-logs-cell-mono">' + escapeHtml(String(item.conversationId || '-')) + '</td>' +
|
||||
'<td><span class="hitl-decision-tag ' + decisionCls + '">' + escapeHtml(hitlDecisionLabel(decision)) + '</span></td>' +
|
||||
'<td>' + escapeHtml(hitlDecidedByLabel(item.decidedBy)) + '</td>' +
|
||||
'<td>' + escapeHtml(hitlDecidedByLabel(item.decidedBy)) + (function () {
|
||||
const engine = hitlAuditEngineFromItem(item);
|
||||
const label = hitlAuditEngineLabel(engine.backend, engine.model);
|
||||
return label ? '<div class="hitl-log-engine">' + escapeHtml(label) + '</div>' : '';
|
||||
}()) + '</td>' +
|
||||
'<td class="hitl-logs-summary">' + escapeHtml(summary) + '</td>' +
|
||||
'<td>' + escapeHtml(hitlFormatTime(item.decidedAt || item.createdAt)) + '</td>' +
|
||||
'<td class="hitl-logs-actions">' +
|
||||
@@ -1676,6 +1782,8 @@ function refreshHitlI18n() {
|
||||
syncAllHitlLogFilterSelects();
|
||||
renderHitlLogsPagination();
|
||||
renderHitlPendingPagination();
|
||||
renderHitlPageAuditEngine();
|
||||
renderHitlStrategyJevHint();
|
||||
}
|
||||
|
||||
function renderHitlLogsPagination() {
|
||||
@@ -1788,6 +1896,33 @@ async function openHitlLogModal(idOpt) {
|
||||
decisionEl.innerHTML = '<span class="hitl-decision-tag ' + cls + '">' + escapeHtml(hitlDecisionLabel(decision)) + '</span>';
|
||||
}
|
||||
if (decidedByEl) decidedByEl.textContent = hitlDecidedByLabel(item.decidedBy);
|
||||
let engineRow = document.getElementById('hitl-log-detail-engine-row');
|
||||
let engineEl = document.getElementById('hitl-log-detail-engine');
|
||||
if (!engineRow || !engineEl) {
|
||||
const decidedRow = decidedByEl && decidedByEl.closest('.hitl-log-detail-row');
|
||||
const dl = decidedRow && decidedRow.parentElement;
|
||||
if (dl && decidedRow) {
|
||||
engineRow = document.createElement('div');
|
||||
engineRow.className = 'hitl-log-detail-row';
|
||||
engineRow.id = 'hitl-log-detail-engine-row';
|
||||
engineRow.hidden = true;
|
||||
engineRow.innerHTML = '<dt>' + escapeHtml(hitlT('colAuditEngine', 'Approval engine')) + '</dt><dd id="hitl-log-detail-engine">—</dd>';
|
||||
if (decidedRow.nextSibling) dl.insertBefore(engineRow, decidedRow.nextSibling);
|
||||
else dl.appendChild(engineRow);
|
||||
engineEl = document.getElementById('hitl-log-detail-engine');
|
||||
}
|
||||
}
|
||||
if (engineRow && engineEl) {
|
||||
const engine = hitlAuditEngineFromItem(item);
|
||||
const label = hitlAuditEngineLabel(engine.backend, engine.model);
|
||||
if (label) {
|
||||
engineEl.textContent = label;
|
||||
engineRow.hidden = false;
|
||||
} else {
|
||||
engineEl.textContent = '';
|
||||
engineRow.hidden = true;
|
||||
}
|
||||
}
|
||||
if (timeEl) timeEl.textContent = hitlFormatTime(item.decidedAt || item.createdAt);
|
||||
const comment = String(item.comment || '').trim();
|
||||
if (commentRow && commentEl) {
|
||||
@@ -1823,6 +1958,8 @@ window.refreshHitlPageWhitelist = refreshHitlPageWhitelist;
|
||||
window.refreshHitlPending = refreshHitlPending;
|
||||
window.refreshHitlLogs = refreshHitlLogs;
|
||||
window.refreshHitlActivePanel = refreshHitlActivePanel;
|
||||
window.renderHitlPageAuditEngine = renderHitlPageAuditEngine;
|
||||
window.renderHitlStrategyJevHint = renderHitlStrategyJevHint;
|
||||
window.switchHitlPageTab = switchHitlPageTab;
|
||||
window.switchHitlStrategyMode = switchHitlStrategyMode;
|
||||
window.resetHitlAuditStrategy = resetHitlAuditStrategy;
|
||||
|
||||
@@ -110,6 +110,12 @@
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof window.syncHitlAuditBackendUI === 'function') {
|
||||
window.syncHitlAuditBackendUI();
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function updateLangLabel() {
|
||||
|
||||
@@ -790,6 +790,11 @@ async function loadConfig(loadTools = true, options = {}) {
|
||||
hitlReviewerEl.value = reviewer === 'audit_agent' ? 'audit_agent' : 'human';
|
||||
}
|
||||
const hitlAuditModel = hitl.audit_model || {};
|
||||
const hitlAuditBackendEl = document.getElementById('hitl-audit-backend');
|
||||
if (hitlAuditBackendEl) {
|
||||
const backend = String(hitl.audit_backend || '').trim().toLowerCase();
|
||||
hitlAuditBackendEl.value = (backend === 'typesafe' || backend === 'jev') ? 'typesafe' : 'openai';
|
||||
}
|
||||
const hitlAuditProviderEl = document.getElementById('hitl-audit-model-provider');
|
||||
if (hitlAuditProviderEl) {
|
||||
const provider = String(hitlAuditModel.provider || '').trim().toLowerCase();
|
||||
@@ -817,6 +822,9 @@ async function loadConfig(loadTools = true, options = {}) {
|
||||
if (hitlReviewEditPromptEl) {
|
||||
hitlReviewEditPromptEl.value = hitl.audit_agent_prompt_review_edit || '';
|
||||
}
|
||||
if (typeof window.syncHitlAuditBackendUI === 'function') {
|
||||
window.syncHitlAuditBackendUI();
|
||||
}
|
||||
|
||||
// 填充Agent配置
|
||||
document.getElementById('agent-max-iterations').value = currentConfig.agent.max_iterations || 30;
|
||||
@@ -2020,6 +2028,7 @@ async function applySettings() {
|
||||
},
|
||||
hitl: {
|
||||
...prevHitl,
|
||||
audit_backend: document.getElementById('hitl-audit-backend')?.value === 'typesafe' ? 'typesafe' : 'openai',
|
||||
audit_model: {
|
||||
...(prevHitl.audit_model || {}),
|
||||
provider: document.getElementById('hitl-audit-model-provider')?.value || '',
|
||||
@@ -3287,6 +3296,15 @@ function initModelListControls() {
|
||||
hitlAuditProv.dataset.modelListBound = '1';
|
||||
hitlAuditProv.addEventListener('change', syncModelListFetchButtons);
|
||||
}
|
||||
const hitlAuditBackend = document.getElementById('hitl-audit-backend');
|
||||
if (hitlAuditBackend && !hitlAuditBackend.dataset.backendBound) {
|
||||
hitlAuditBackend.dataset.backendBound = '1';
|
||||
hitlAuditBackend.addEventListener('change', function () {
|
||||
syncHitlAuditBackendUI();
|
||||
syncModelListFetchButtons();
|
||||
});
|
||||
syncHitlAuditBackendUI();
|
||||
}
|
||||
const knowledgeEmbeddingProv = document.getElementById('knowledge-embedding-provider');
|
||||
if (knowledgeEmbeddingProv && !knowledgeEmbeddingProv.dataset.modelListBound) {
|
||||
knowledgeEmbeddingProv.dataset.modelListBound = '1';
|
||||
@@ -3638,6 +3656,48 @@ async function testVisionConnection() {
|
||||
}
|
||||
}
|
||||
|
||||
function isHitlAuditTypeSafe() {
|
||||
const v = (document.getElementById('hitl-audit-backend')?.value || '').trim().toLowerCase();
|
||||
return v === 'typesafe' || v === 'jev';
|
||||
}
|
||||
|
||||
function syncHitlAuditBackendUI() {
|
||||
const ts = isHitlAuditTypeSafe();
|
||||
const providerGroup = document.getElementById('hitl-audit-openai-provider-group');
|
||||
if (providerGroup) providerGroup.style.display = ts ? 'none' : '';
|
||||
const fetchBtn = document.getElementById('fetch-hitl-audit-models-btn');
|
||||
if (fetchBtn) fetchBtn.style.display = ts ? 'none' : '';
|
||||
const openaiHint = document.getElementById('hitl-audit-model-openai-hint');
|
||||
const tsHint = document.getElementById('hitl-audit-model-typesafe-hint');
|
||||
if (openaiHint) openaiHint.hidden = ts;
|
||||
if (tsHint) tsHint.hidden = !ts;
|
||||
const promptHint = document.getElementById('hitl-audit-prompt-typesafe-hint');
|
||||
if (promptHint) promptHint.hidden = !ts;
|
||||
|
||||
const tFn = function (key, fallback) {
|
||||
return typeof settingsT === 'function' ? settingsT(key, fallback) : (fallback || key);
|
||||
};
|
||||
const baseUrlEl = document.getElementById('hitl-audit-model-base-url');
|
||||
const apiKeyEl = document.getElementById('hitl-audit-model-api-key');
|
||||
const modelEl = document.getElementById('hitl-audit-model-name');
|
||||
if (baseUrlEl) {
|
||||
baseUrlEl.placeholder = ts
|
||||
? tFn('settings.hitl.auditModelTypeSafeBaseUrlPlaceholder', '留空使用 https://api.typesafe.ai')
|
||||
: tFn('settings.hitl.auditModelBaseUrlPlaceholder', '留空则复用主模型 Base URL');
|
||||
}
|
||||
if (apiKeyEl) {
|
||||
apiKeyEl.placeholder = ts
|
||||
? tFn('settings.hitl.auditModelTypeSafeApiKeyPlaceholder', 'TypeSafe API Key(必填,不复用主模型)')
|
||||
: tFn('settings.hitl.auditModelApiKeyPlaceholder', '留空则复用主模型 API Key');
|
||||
}
|
||||
if (modelEl) {
|
||||
modelEl.placeholder = ts
|
||||
? tFn('settings.hitl.auditModelTypeSafeNamePlaceholder', '留空使用 jev-latest')
|
||||
: tFn('settings.hitl.auditModelNamePlaceholder', '留空则复用主模型;建议填写小模型');
|
||||
}
|
||||
}
|
||||
window.syncHitlAuditBackendUI = syncHitlAuditBackendUI;
|
||||
|
||||
function collectHitlAuditModelEffectiveConfig() {
|
||||
const main = {
|
||||
provider: document.getElementById('openai-provider')?.value || 'openai',
|
||||
@@ -3656,9 +3716,29 @@ function collectHitlAuditModelEffectiveConfig() {
|
||||
async function testHitlAuditModelConnection() {
|
||||
const btn = document.getElementById('test-hitl-audit-model-btn');
|
||||
const resultEl = document.getElementById('test-hitl-audit-model-result');
|
||||
const typeSafe = isHitlAuditTypeSafe();
|
||||
const cfg = collectHitlAuditModelEffectiveConfig();
|
||||
const apiKey = typeSafe
|
||||
? (document.getElementById('hitl-audit-model-api-key')?.value.trim() || '')
|
||||
: cfg.api_key;
|
||||
const baseUrl = typeSafe
|
||||
? (document.getElementById('hitl-audit-model-base-url')?.value.trim() || '')
|
||||
: cfg.base_url;
|
||||
const model = typeSafe
|
||||
? (document.getElementById('hitl-audit-model-name')?.value.trim() || 'jev-latest')
|
||||
: cfg.model;
|
||||
|
||||
if (!cfg.base_url || !cfg.api_key || !cfg.model) {
|
||||
if (typeSafe) {
|
||||
if (!apiKey) {
|
||||
if (resultEl) {
|
||||
resultEl.style.color = 'var(--danger-color, #e53e3e)';
|
||||
resultEl.textContent = typeof settingsT === 'function'
|
||||
? settingsT('settings.hitl.testTypeSafeFillRequired', '请先填写 TypeSafe API Key')
|
||||
: '请先填写 TypeSafe API Key';
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (!cfg.base_url || !cfg.api_key || !cfg.model) {
|
||||
if (resultEl) {
|
||||
resultEl.style.color = 'var(--danger-color, #e53e3e)';
|
||||
resultEl.textContent = typeof window.t === 'function' ? window.t('settingsBasic.testFillRequired') : '请先填写 Base URL、API Key 和模型';
|
||||
@@ -3676,10 +3756,14 @@ async function testHitlAuditModelConnection() {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiFetch('/api/config/test-openai', {
|
||||
const endpoint = typeSafe ? '/api/config/test-typesafe' : '/api/config/test-openai';
|
||||
const payload = typeSafe
|
||||
? { base_url: baseUrl, api_key: apiKey, model: model }
|
||||
: cfg;
|
||||
const response = await apiFetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(cfg)
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
@@ -1464,6 +1464,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hitl-page-audit-engine" id="hitl-page-audit-engine" hidden></p>
|
||||
<p class="hitl-page-reviewer-hint" data-i18n="hitl.pageReviewerHint">作用于当前选中会话;未选会话时保存到本机,新建会话时沿用。切换后立即生效。</p>
|
||||
</div>
|
||||
<div class="hitl-page-tabs" role="tablist">
|
||||
@@ -1553,6 +1554,7 @@
|
||||
</div>
|
||||
<p class="hitl-page-strategy-hint" id="hitl-strategy-hint-approval" data-i18n="hitl.strategyHintApproval">白名单内工具免审批;审批模式下审计 Agent 仅裁决通过/拒绝。</p>
|
||||
<p class="hitl-page-strategy-hint" id="hitl-strategy-hint-review-edit" hidden data-i18n="hitl.strategyHintReviewEdit">审查编辑模式下审计 Agent 可通过 editedArguments 收窄参数后放行;无法安全改参时应拒绝。</p>
|
||||
<p class="hitl-page-strategy-hint" id="hitl-strategy-hint-jev" hidden data-i18n="hitl.strategyHintJev">当前是 TypeSafe Jev:会读取下面的自定义策略并编成结构化问题;破坏业务可用性等内置规则仍是硬底线。Jev 不能改参。</p>
|
||||
<textarea id="hitl-audit-agent-prompt" class="hitl-strategy-textarea" rows="14" spellcheck="false" autocomplete="off"></textarea>
|
||||
<textarea id="hitl-audit-agent-prompt-review-edit" class="hitl-strategy-textarea" rows="14" spellcheck="false" autocomplete="off" hidden></textarea>
|
||||
<div id="hitl-strategy-feedback" class="hitl-apply-feedback" role="status" aria-live="polite" hidden></div>
|
||||
@@ -1603,6 +1605,10 @@
|
||||
<dt data-i18n="hitl.colDecidedBy">审批方</dt>
|
||||
<dd id="hitl-log-detail-decided-by">—</dd>
|
||||
</div>
|
||||
<div class="hitl-log-detail-row" id="hitl-log-detail-engine-row" hidden>
|
||||
<dt data-i18n="hitl.colAuditEngine">审批引擎</dt>
|
||||
<dd id="hitl-log-detail-engine">—</dd>
|
||||
</div>
|
||||
<div class="hitl-log-detail-row">
|
||||
<dt data-i18n="hitl.colTime">时间</dt>
|
||||
<dd id="hitl-log-detail-time">—</dd>
|
||||
@@ -3983,6 +3989,14 @@
|
||||
<h5 data-i18n="settings.hitl.auditModelTitle">审计 Agent 模型</h5>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="hitl-audit-backend" data-i18n="settings.hitl.auditBackend">审批引擎</label>
|
||||
<select id="hitl-audit-backend" class="form-select">
|
||||
<option value="openai" data-i18n="settings.hitl.auditBackendOpenAI">OpenAI 协议模型</option>
|
||||
<option value="typesafe" data-i18n="settings.hitl.auditBackendTypeSafe">TypeSafe Jev</option>
|
||||
</select>
|
||||
<small class="form-hint" data-i18n="settings.hitl.auditBackendHint">二选一:OpenAI 兼容聊天模型按提示词输出 JSON;Jev 用结构化问题做放通/拦截,不能改参。</small>
|
||||
</div>
|
||||
<div class="form-group" id="hitl-audit-openai-provider-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>
|
||||
@@ -4009,7 +4023,8 @@
|
||||
</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>
|
||||
<small id="hitl-audit-model-openai-hint" class="form-hint" data-i18n="settings.hitl.auditModelHint">仅审计 Agent 审批时使用;人工审批不消耗模型。</small>
|
||||
<small id="hitl-audit-model-typesafe-hint" class="form-hint" data-i18n="settings.hitl.auditModelTypeSafeHint" hidden>Jev 走 TypeSafe System One,不是 OpenAI 协议。API Key 必填,不复用主模型密钥;模型留空使用 jev-latest。审查编辑模式下 Jev 只做放通/拦截,不会改参。</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>
|
||||
@@ -4035,6 +4050,7 @@
|
||||
<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>
|
||||
<small id="hitl-audit-prompt-typesafe-hint" class="form-hint" data-i18n="settings.hitl.auditPromptTypeSafeHint" hidden>当前审批引擎是 TypeSafe Jev:会读取本页/人机协同页的自定义策略并编成结构化问题;内置破坏性规则仍是硬底线。Jev 不能改参。</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="hitl-audit-agent-prompt-review-edit-settings" data-i18n="settings.hitl.auditPromptReviewEdit">审查编辑模式提示词</label>
|
||||
|
||||
Reference in New Issue
Block a user