mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-29 06:00:52 +02:00
feat: persist hitl default config
This commit is contained in:
+27
-82
@@ -154,9 +154,6 @@ const chatSystemModelCache = new Map();
|
||||
|
||||
// 人机协同(HITL)会话级配置
|
||||
const HITL_STORAGE_PREFIX = 'cyberstrike-chat-hitl';
|
||||
const HITL_DRAFT_KEY = 'cyberstrike-chat-hitl-draft';
|
||||
/** 跨会话记忆:用户最近一次在侧栏选择的 HITL 偏好(与 hitl.js 中 readHitlGlobalLast 使用同一 key) */
|
||||
const HITL_GLOBAL_LAST_KEY = `${HITL_STORAGE_PREFIX}:__last__`;
|
||||
const HITL_MODE_OFF = 'off';
|
||||
const HITL_MODE_APPROVAL = 'approval';
|
||||
const HITL_MODE_REVIEW_EDIT = 'review_edit';
|
||||
@@ -432,14 +429,17 @@ function normalizeHitlTimeoutForChat(value, fallback) {
|
||||
}
|
||||
|
||||
function defaultHitlConfig() {
|
||||
const serverReviewer = (typeof window !== 'undefined' && window.csaiHitlDefaultReviewer)
|
||||
const serverDefault = (typeof window !== 'undefined' && window.csaiHitlDefaultConfig && typeof window.csaiHitlDefaultConfig === 'object')
|
||||
? window.csaiHitlDefaultConfig
|
||||
: {};
|
||||
const serverReviewer = serverDefault.reviewer || ((typeof window !== 'undefined' && window.csaiHitlDefaultReviewer)
|
||||
? window.csaiHitlDefaultReviewer
|
||||
: 'human';
|
||||
: 'human');
|
||||
return {
|
||||
mode: HITL_MODE_OFF,
|
||||
mode: normalizeHitlMode(serverDefault.mode || HITL_MODE_OFF),
|
||||
reviewer: normalizeHitlReviewer(serverReviewer),
|
||||
sensitiveTools: DEFAULT_HITL_SESSION_TOOL_WHITELIST,
|
||||
timeoutSeconds: DEFAULT_HITL_TIMEOUT_SECONDS,
|
||||
timeoutSeconds: normalizeHitlTimeoutForChat(serverDefault.timeoutSeconds, DEFAULT_HITL_TIMEOUT_SECONDS),
|
||||
updatedAt: ''
|
||||
};
|
||||
}
|
||||
@@ -520,70 +520,11 @@ function getHitlModeLabel(mode) {
|
||||
}
|
||||
}
|
||||
|
||||
function getHitlLastGlobalConfig() {
|
||||
const fallback = defaultHitlConfig();
|
||||
try {
|
||||
const raw = localStorage.getItem(HITL_GLOBAL_LAST_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== 'object') return null;
|
||||
return {
|
||||
mode: normalizeHitlMode(parsed.mode),
|
||||
reviewer: normalizeHitlReviewer(parsed.reviewer),
|
||||
sensitiveTools: typeof parsed.sensitiveTools === 'string' ? parsed.sensitiveTools : fallback.sensitiveTools,
|
||||
timeoutSeconds: normalizeHitlTimeoutForChat(parsed.timeoutSeconds, fallback.timeoutSeconds),
|
||||
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : ''
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveHitlLastGlobalConfig(payload) {
|
||||
if (!payload || typeof payload !== 'object') return;
|
||||
try {
|
||||
localStorage.setItem(HITL_GLOBAL_LAST_KEY, JSON.stringify(payload));
|
||||
} catch (e) {
|
||||
console.warn('saveHitlLastGlobalConfig failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
function getHitlConfigForConversation(conversationId) {
|
||||
const fallback = defaultHitlConfig();
|
||||
const cid = conversationId ? String(conversationId).trim() : '';
|
||||
if (!cid) {
|
||||
const globalLast = getHitlLastGlobalConfig();
|
||||
let draftCfg = null;
|
||||
try {
|
||||
const raw = localStorage.getItem(HITL_DRAFT_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
draftCfg = {
|
||||
mode: normalizeHitlMode(parsed.mode),
|
||||
reviewer: normalizeHitlReviewer(parsed.reviewer),
|
||||
sensitiveTools: typeof parsed.sensitiveTools === 'string' ? parsed.sensitiveTools : fallback.sensitiveTools,
|
||||
timeoutSeconds: normalizeHitlTimeoutForChat(parsed.timeoutSeconds, fallback.timeoutSeconds),
|
||||
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : ''
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
draftCfg = null;
|
||||
}
|
||||
const g = globalLast ? {
|
||||
mode: normalizeHitlMode(globalLast.mode),
|
||||
reviewer: normalizeHitlReviewer(globalLast.reviewer),
|
||||
sensitiveTools: typeof globalLast.sensitiveTools === 'string' ? globalLast.sensitiveTools : fallback.sensitiveTools,
|
||||
timeoutSeconds: normalizeHitlTimeoutForChat(globalLast.timeoutSeconds, fallback.timeoutSeconds),
|
||||
updatedAt: typeof globalLast.updatedAt === 'string' ? globalLast.updatedAt : ''
|
||||
} : null;
|
||||
if (!draftCfg && !g) return fallback;
|
||||
if (!draftCfg) return g;
|
||||
if (!g) return draftCfg;
|
||||
const tg = Date.parse(g.updatedAt) || 0;
|
||||
const td = Date.parse(draftCfg.updatedAt) || 0;
|
||||
return tg > td ? g : draftCfg;
|
||||
return fallback;
|
||||
}
|
||||
const key = getHitlStorageKeyByConversation(cid);
|
||||
try {
|
||||
@@ -627,6 +568,8 @@ async function onHitlReviewerChanged(reviewer) {
|
||||
try {
|
||||
if (cid && typeof window.saveHitlConversationConfig === 'function') {
|
||||
await window.saveHitlConversationConfig(cid, cfg);
|
||||
} else if (typeof window.putHitlDefaultConfig === 'function') {
|
||||
await window.putHitlDefaultConfig(cfg);
|
||||
} else if (typeof window.putHitlDefaultReviewer === 'function') {
|
||||
await window.putHitlDefaultReviewer(cfg.reviewer);
|
||||
}
|
||||
@@ -652,7 +595,10 @@ function bindHitlReviewerToggleListeners() {
|
||||
}
|
||||
|
||||
function saveHitlConfigForConversation(conversationId, cfg, opts) {
|
||||
const syncGlobalLast = !!(opts && opts.syncGlobalLast);
|
||||
void opts;
|
||||
if (!conversationId) {
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
mode: normalizeHitlMode(cfg && cfg.mode),
|
||||
reviewer: normalizeHitlReviewer(cfg && cfg.reviewer),
|
||||
@@ -660,12 +606,9 @@ function saveHitlConfigForConversation(conversationId, cfg, opts) {
|
||||
timeoutSeconds: normalizeHitlTimeoutForChat(cfg && cfg.timeoutSeconds, DEFAULT_HITL_TIMEOUT_SECONDS),
|
||||
updatedAt: typeof (cfg && cfg.updatedAt) === 'string' ? cfg.updatedAt : ''
|
||||
};
|
||||
const key = conversationId ? getHitlStorageKeyByConversation(conversationId) : HITL_DRAFT_KEY;
|
||||
const key = getHitlStorageKeyByConversation(conversationId);
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(payload));
|
||||
if (syncGlobalLast) {
|
||||
saveHitlLastGlobalConfig(payload);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('saveHitlConfigForConversation failed', e);
|
||||
}
|
||||
@@ -752,8 +695,9 @@ async function waitForHitlConfigReady(conversationId) {
|
||||
await hitlConfigSyncPromise;
|
||||
return;
|
||||
}
|
||||
if (!cid && window.csaiHitlDefaultReviewerReady && typeof window.csaiHitlDefaultReviewerReady.then === 'function') {
|
||||
await window.csaiHitlDefaultReviewerReady.catch(function () {});
|
||||
const defaultReady = window.csaiHitlDefaultConfigReady || window.csaiHitlDefaultReviewerReady;
|
||||
if (!cid && defaultReady && typeof defaultReady.then === 'function') {
|
||||
await defaultReady.catch(function () {});
|
||||
if (!currentConversationId) refreshHitlConfigByCurrentConversation();
|
||||
}
|
||||
}
|
||||
@@ -818,6 +762,10 @@ async function applyHitlSidebarConfig() {
|
||||
await window.saveHitlConversationConfig(cid, cfg);
|
||||
const ok = typeof window.t === 'function' ? window.t('chat.hitlApplyOkSync') : '人机协同配置已保存并同步到服务器。';
|
||||
showHitlApplyFeedback(ok, false);
|
||||
} else if (typeof window.putHitlDefaultConfig === 'function') {
|
||||
await window.putHitlDefaultConfig(cfg);
|
||||
const okDefault = typeof window.t === 'function' ? window.t('chat.hitlApplyOkDefaultConfig') : '人机协同默认配置已写入 config.yaml 并生效。';
|
||||
showHitlApplyFeedback(okDefault, false);
|
||||
} else if (yamlMerged) {
|
||||
const okYaml = typeof window.t === 'function' ? window.t('chat.hitlApplyOkWhitelistYaml') : '免审批工具已合并进 config.yaml 并生效。会话配置会自动保存。';
|
||||
showHitlApplyFeedback(okYaml, false);
|
||||
@@ -913,6 +861,11 @@ function applyConversationAgentMode(conversationId, conversation) {
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.csaiHitlGlobalToolWhitelist = window.csaiHitlGlobalToolWhitelist || [];
|
||||
window.csaiHitlDefaultConfig = window.csaiHitlDefaultConfig || {
|
||||
mode: HITL_MODE_OFF,
|
||||
reviewer: 'human',
|
||||
timeoutSeconds: DEFAULT_HITL_TIMEOUT_SECONDS
|
||||
};
|
||||
window.csaiHitlDefaultReviewer = window.csaiHitlDefaultReviewer || 'human';
|
||||
window.csaiChatAgentMode = {
|
||||
EINO_MODES: CHAT_AGENT_EINO_MODES,
|
||||
@@ -934,7 +887,6 @@ if (typeof window !== 'undefined') {
|
||||
window.setHitlReviewerUI = setHitlReviewerUI;
|
||||
window.onHitlReviewerChanged = onHitlReviewerChanged;
|
||||
window.bindHitlReviewerToggleListeners = bindHitlReviewerToggleListeners;
|
||||
window.getHitlLastGlobalConfig = getHitlLastGlobalConfig;
|
||||
window.hitlMergeToolsForDisplay = hitlMergeToolsForDisplay;
|
||||
window.hitlStripGlobalToolsFromFormString = hitlStripGlobalToolsFromFormString;
|
||||
window.hitlToolsSplitToArray = hitlToolsSplitToArray;
|
||||
@@ -5964,13 +5916,6 @@ async function startNewConversation(options = {}) {
|
||||
chatInput.value = '';
|
||||
adjustTextareaHeight(chatInput);
|
||||
}
|
||||
// 把当前侧栏人机协同选项写入草稿与「最近应用」记忆,避免刷新时被旧草稿里的「关闭」覆盖
|
||||
try {
|
||||
if (typeof readHitlConfigFromForm === 'function' && typeof saveHitlConfigForConversation === 'function') {
|
||||
const snap = readHitlConfigFromForm();
|
||||
saveHitlConfigForConversation('', snap, { syncGlobalLast: true });
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
refreshHitlConfigByCurrentConversation();
|
||||
}
|
||||
|
||||
|
||||
+67
-19
@@ -248,47 +248,89 @@ async function fetchHitlConversationConfig(conversationId) {
|
||||
if (!data || !data.hitl) return null;
|
||||
return {
|
||||
hitl: data.hitl,
|
||||
defaultMode: hitlModeNormalize(data.defaultMode || 'off'),
|
||||
defaultReviewer: hitlReviewerNormalize(data.defaultReviewer || 'human'),
|
||||
defaultTimeoutSeconds: normalizeHitlTimeoutSeconds(data.defaultTimeoutSeconds, 300),
|
||||
hitlGlobalToolWhitelist: Array.isArray(data.hitlGlobalToolWhitelist) ? data.hitlGlobalToolWhitelist : []
|
||||
};
|
||||
}
|
||||
|
||||
function applyHitlDefaultReviewerFromServer(reviewer) {
|
||||
const v = hitlReviewerNormalize(reviewer);
|
||||
return applyHitlDefaultConfigFromServer({ defaultReviewer: reviewer });
|
||||
}
|
||||
|
||||
function applyHitlDefaultConfigFromServer(data) {
|
||||
const src = data && typeof data === 'object' ? data : {};
|
||||
const mode = hitlModeNormalize(src.defaultMode || src.mode || 'off');
|
||||
const reviewer = hitlReviewerNormalize(src.defaultReviewer || src.reviewer || 'human');
|
||||
const timeoutSeconds = normalizeHitlTimeoutSeconds(
|
||||
src.defaultTimeoutSeconds != null ? src.defaultTimeoutSeconds : src.timeoutSeconds,
|
||||
300
|
||||
);
|
||||
const out = {
|
||||
mode: mode,
|
||||
reviewer: reviewer,
|
||||
timeoutSeconds: timeoutSeconds
|
||||
};
|
||||
if (typeof window !== 'undefined') {
|
||||
window.csaiHitlDefaultReviewer = v;
|
||||
window.csaiHitlDefaultConfig = out;
|
||||
window.csaiHitlDefaultReviewer = reviewer;
|
||||
if (Array.isArray(src.hitlGlobalToolWhitelist)) {
|
||||
window.csaiHitlGlobalToolWhitelist = src.hitlGlobalToolWhitelist;
|
||||
}
|
||||
}
|
||||
return v;
|
||||
return out;
|
||||
}
|
||||
|
||||
async function fetchHitlDefaultConfig() {
|
||||
const resp = await hitlApiFetch('/api/hitl/default-config', { credentials: 'same-origin' });
|
||||
if (!resp.ok) {
|
||||
return applyHitlDefaultConfigFromServer({ defaultMode: 'off', defaultReviewer: 'human', defaultTimeoutSeconds: 300 });
|
||||
}
|
||||
const data = await resp.json();
|
||||
return applyHitlDefaultConfigFromServer(data);
|
||||
}
|
||||
|
||||
async function fetchHitlDefaultReviewer() {
|
||||
const resp = await hitlApiFetch('/api/hitl/default-reviewer', { credentials: 'same-origin' });
|
||||
if (!resp.ok) {
|
||||
return applyHitlDefaultReviewerFromServer('human');
|
||||
}
|
||||
const data = await resp.json();
|
||||
return applyHitlDefaultReviewerFromServer(data && data.defaultReviewer);
|
||||
const cfg = await fetchHitlDefaultConfig();
|
||||
return hitlReviewerNormalize(cfg && cfg.reviewer);
|
||||
}
|
||||
|
||||
async function putHitlDefaultReviewer(reviewer) {
|
||||
const normalized = hitlReviewerNormalize(reviewer);
|
||||
const resp = await hitlApiFetch('/api/hitl/default-reviewer', {
|
||||
async function putHitlDefaultConfig(config) {
|
||||
const current = (typeof window !== 'undefined' && window.csaiHitlDefaultConfig && typeof window.csaiHitlDefaultConfig === 'object')
|
||||
? window.csaiHitlDefaultConfig
|
||||
: { mode: 'off', reviewer: 'human', timeoutSeconds: 300 };
|
||||
const cfg = config && typeof config === 'object' ? config : {};
|
||||
const payload = {
|
||||
mode: hitlModeNormalize(cfg.mode != null ? cfg.mode : current.mode),
|
||||
reviewer: hitlReviewerNormalize(cfg.reviewer != null ? cfg.reviewer : current.reviewer),
|
||||
timeoutSeconds: normalizeHitlTimeoutSeconds(
|
||||
cfg.timeoutSeconds != null ? cfg.timeoutSeconds : current.timeoutSeconds,
|
||||
300
|
||||
)
|
||||
};
|
||||
const resp = await hitlApiFetch('/api/hitl/default-config', {
|
||||
method: 'PUT',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ reviewer: normalized })
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const msg = await readHitlApiError(resp);
|
||||
throw new Error(msg || ('HTTP ' + resp.status));
|
||||
}
|
||||
const data = await resp.json();
|
||||
return applyHitlDefaultReviewerFromServer(data && data.defaultReviewer);
|
||||
return applyHitlDefaultConfigFromServer(data);
|
||||
}
|
||||
|
||||
async function putHitlDefaultReviewer(reviewer) {
|
||||
const cfg = await putHitlDefaultConfig({ reviewer: reviewer });
|
||||
return hitlReviewerNormalize(cfg && cfg.reviewer);
|
||||
}
|
||||
|
||||
async function initHitlDefaultReviewerFromServer() {
|
||||
try {
|
||||
await fetchHitlDefaultReviewer();
|
||||
await fetchHitlDefaultConfig();
|
||||
if (!getCurrentConversationIdForHitl() && typeof window.refreshHitlConfigByCurrentConversation === 'function') {
|
||||
window.refreshHitlConfigByCurrentConversation();
|
||||
}
|
||||
@@ -535,10 +577,13 @@ async function syncHitlConfigFromServer(conversationId) {
|
||||
const pack = await fetchHitlConversationConfig(conversationId);
|
||||
if (!pack || !pack.hitl) return;
|
||||
const cfg = pack.hitl;
|
||||
if (pack.defaultReviewer) {
|
||||
applyHitlDefaultReviewerFromServer(pack.defaultReviewer);
|
||||
}
|
||||
const globalWL = pack.hitlGlobalToolWhitelist || [];
|
||||
applyHitlDefaultConfigFromServer({
|
||||
defaultMode: pack.defaultMode,
|
||||
defaultReviewer: pack.defaultReviewer,
|
||||
defaultTimeoutSeconds: pack.defaultTimeoutSeconds,
|
||||
hitlGlobalToolWhitelist: globalWL
|
||||
});
|
||||
if (typeof window !== 'undefined') {
|
||||
window.csaiHitlGlobalToolWhitelist = globalWL;
|
||||
}
|
||||
@@ -1820,7 +1865,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
if (typeof window.bindHitlReviewerToggleListeners === 'function') {
|
||||
window.bindHitlReviewerToggleListeners();
|
||||
}
|
||||
window.csaiHitlDefaultReviewerReady = initHitlDefaultReviewerFromServer();
|
||||
window.csaiHitlDefaultConfigReady = initHitlDefaultReviewerFromServer();
|
||||
window.csaiHitlDefaultReviewerReady = window.csaiHitlDefaultConfigReady;
|
||||
setTimeout(reconcileHitlUiState, 0);
|
||||
});
|
||||
|
||||
@@ -1836,6 +1882,8 @@ document.addEventListener('languagechange', function () {
|
||||
window.syncHitlConfigToServerByCurrentConversation = syncHitlConfigToServerByCurrentConversation;
|
||||
window.saveHitlConversationConfig = saveHitlConversationConfig;
|
||||
window.mergeHitlGlobalToolWhitelist = mergeHitlGlobalToolWhitelist;
|
||||
window.fetchHitlDefaultConfig = fetchHitlDefaultConfig;
|
||||
window.putHitlDefaultConfig = putHitlDefaultConfig;
|
||||
|
||||
// 由 chat.js 在 loadConversation 内 await 调用;挂到 window 供其它入口显式触发
|
||||
window.syncHitlConfigFromServer = syncHitlConfigFromServer;
|
||||
|
||||
Reference in New Issue
Block a user