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:
Ed1s0nZandCursor committed 2026-09-22 21:15:53 +08:00
1 parent 3aa9274675
commit 38b96ec67a
30 files changed
+1372 -21

No files matched your search

+31 -1
View File
@@ -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;
+19 -1
View File
@@ -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
View File
@@ -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;
+6
View File
@@ -110,6 +110,12 @@
} catch (e) {
// ignore
}
try {
if (typeof window.syncHitlAuditBackendUI === 'function') {
window.syncHitlAuditBackendUI();
}
} catch (e) { /* ignore */ }
}
function updateLangLabel() {
+87 -3
View File
@@ -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();