mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-14 23:20:25 +02:00
优化项目对话、刷新续流、实时滚动与工具状态恢复 (#245)
* feat(chat): add project-based conversation sidebar * feat(chat): refine Codex-style conversation UI * feat(chat): add Codex-style conversation workflow * feat(ui): 优化对话框与项目侧边栏交互 * fix(chat): 修复暗色输入框圆角填色 * fix(chat): 恢复输入区分层错位布局 * fix(hitl): isolate reviewer state per conversation * feat(hitl): 增加双入口审批与倒计时进度 * fix(hitl): 汇总项目审批并隔离对话状态 * fix(ui): 修复审批状态与无项目新任务 * fix(ui): 优化审批状态与对话切换性能 * fix(ui): 修复中断任务审批仍计时 * fix(ui): 修复多对话并发切换卡顿 * fix(hitl): 主动同步审批并关闭中断状态 * fix(ui): 固定项目审批汇总为绿色 * feat(ui): 同步系统模型与推理强度 * feat(ui): 完善项目侧栏预览与新建入口 * fix(ui): 防止无项目文件夹误展开 * fix(ui): 防止长历史对话滚动误触审批 * fix(ui): 修复对话操作并补充项目置顶 * fix(ui): 移除对话分组并调整项目置顶排序 * feat(ui): 优化迭代导航与审批交互 * fix(hitl): 将 write_file 加入内置免审批工具 * fix(chat): 支持回车发送与 Shift 回车换行 * fix(chat): 优化对话刷新与 Codex 风格交互 * fix(ui): 显示对话具体更新时间 * fix(ui): 优化对话刷新与项目加载 * fix(ui): 修复 Agent 审查文字裁切 * fix(chat): 修复刷新续流与多标签页同步 * fix(chat): 修复滚动跟随与中断任务终态 * fix(ui): 修复流式滚动跳动与工具状态恢复 * fix(ui): 修复刷新后流式输出停止粘底
This commit is contained in:
+48
-38
@@ -98,6 +98,7 @@ function hitlT(key, fallback, params) {
|
||||
|
||||
const HITL_LOGS_PAGE_SIZE_KEY = 'cyberstrike_hitl_logs_page_size';
|
||||
const HITL_PENDING_PAGE_SIZE_KEY = 'cyberstrike_hitl_pending_page_size';
|
||||
const HITL_TIMEOUT_DEFAULT_MIGRATION_PREFIX = 'cyberstrike-hitl-timeout-default-v1:';
|
||||
const HITL_PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
|
||||
|
||||
function hitlPaginationT(key, opts, fallback) {
|
||||
@@ -212,6 +213,21 @@ function normalizeHitlTimeoutSeconds(v, fallback) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function shouldMigrateLegacyHitlTimeout(conversationId, timeoutSeconds) {
|
||||
if (!conversationId || normalizeHitlTimeoutSeconds(timeoutSeconds, 0) > 0) return false;
|
||||
try {
|
||||
return localStorage.getItem(HITL_TIMEOUT_DEFAULT_MIGRATION_PREFIX + conversationId) !== '1';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function markLegacyHitlTimeoutMigrated(conversationId) {
|
||||
try {
|
||||
localStorage.setItem(HITL_TIMEOUT_DEFAULT_MIGRATION_PREFIX + conversationId, '1');
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function getCurrentConversationIdForHitl() {
|
||||
if (typeof window.currentConversationId === 'string' && window.currentConversationId) {
|
||||
return window.currentConversationId;
|
||||
@@ -241,16 +257,6 @@ function applyHitlDefaultReviewerFromServer(reviewer) {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.csaiHitlDefaultReviewer = v;
|
||||
}
|
||||
if (typeof window.saveHitlLastGlobalConfig === 'function' && typeof window.getHitlLastGlobalConfig === 'function') {
|
||||
const gl = window.getHitlLastGlobalConfig();
|
||||
const base = gl && typeof gl === 'object'
|
||||
? gl
|
||||
: { mode: 'off', sensitiveTools: '', updatedAt: '' };
|
||||
window.saveHitlLastGlobalConfig(Object.assign({}, base, {
|
||||
reviewer: v,
|
||||
updatedAt: new Date().toISOString()
|
||||
}));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -352,9 +358,9 @@ function showHitlPageWhitelistFeedback(text, isError) {
|
||||
el.className = 'hitl-apply-feedback' + (isError ? ' hitl-apply-feedback--error' : '');
|
||||
}
|
||||
|
||||
function syncHitlSidebarWhitelistDisplay(toolsStr) {
|
||||
const sidebarEl = document.getElementById('hitl-sensitive-tools');
|
||||
if (sidebarEl) sidebarEl.value = toolsStr;
|
||||
function syncHitlSidebarWhitelistDisplay(_toolsStr) {
|
||||
// The chat field is conversation-scoped. Updating the global allowlist page
|
||||
// must not replace it with a merged global + conversation display value.
|
||||
}
|
||||
|
||||
async function fetchHitlGlobalToolWhitelist() {
|
||||
@@ -534,45 +540,41 @@ async function syncHitlConfigFromServer(conversationId) {
|
||||
const local = readHitlLocalStorageConv(conversationId);
|
||||
const localMode = local && local.mode ? hitlModeNormalize(local.mode) : 'off';
|
||||
if (localMode !== 'off') {
|
||||
const localReviewer = hitlReviewerNormalize(local && local.reviewer);
|
||||
let localToolsStr = typeof local.sensitiveTools === 'string' ? local.sensitiveTools : '';
|
||||
localToolsStr = strip(globalWL, localToolsStr);
|
||||
merged = {
|
||||
enabled: true,
|
||||
mode: localMode,
|
||||
reviewer: localReviewer,
|
||||
sensitiveTools: localToolsStr.split(/[,\n\r]+/).map(function (s) { return s.trim(); }).filter(Boolean),
|
||||
timeoutSeconds: normalizeHitlTimeoutSeconds(cfg.timeoutSeconds, 0)
|
||||
timeoutSeconds: normalizeHitlTimeoutSeconds(
|
||||
local && local.timeoutSeconds,
|
||||
normalizeHitlTimeoutSeconds(cfg.timeoutSeconds, 0)
|
||||
)
|
||||
};
|
||||
saveHitlConversationConfig(conversationId, {
|
||||
mode: localMode,
|
||||
reviewer: localReviewer,
|
||||
sensitiveTools: localToolsStr,
|
||||
enabled: true,
|
||||
timeoutSeconds: merged.timeoutSeconds
|
||||
}).catch(function (err) {
|
||||
console.warn('HITL 会话配置同步到服务器失败(将仅保留本地 UI):', err);
|
||||
});
|
||||
} else {
|
||||
const gl = typeof window.getHitlLastGlobalConfig === 'function' ? window.getHitlLastGlobalConfig() : null;
|
||||
const glMode = gl && gl.mode ? hitlModeNormalize(gl.mode) : 'off';
|
||||
if (glMode !== 'off') {
|
||||
let glToolsStr = typeof gl.sensitiveTools === 'string' ? gl.sensitiveTools : '';
|
||||
glToolsStr = strip(globalWL, glToolsStr);
|
||||
merged = {
|
||||
enabled: true,
|
||||
mode: glMode,
|
||||
sensitiveTools: glToolsStr.split(/[,\n\r]+/).map(function (s) { return s.trim(); }).filter(Boolean),
|
||||
timeoutSeconds: normalizeHitlTimeoutSeconds(cfg.timeoutSeconds, 0)
|
||||
};
|
||||
saveHitlConversationConfig(conversationId, {
|
||||
mode: glMode,
|
||||
sensitiveTools: glToolsStr,
|
||||
enabled: true,
|
||||
timeoutSeconds: merged.timeoutSeconds
|
||||
}).catch(function (err) {
|
||||
console.warn('HITL 会话配置同步到服务器失败(将仅保留本地 UI):', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldMigrateLegacyHitlTimeout(conversationId, merged.timeoutSeconds)) {
|
||||
merged = Object.assign({}, merged, { timeoutSeconds: 300 });
|
||||
try {
|
||||
await saveHitlConversationConfig(conversationId, merged);
|
||||
markLegacyHitlTimeoutMigrated(conversationId);
|
||||
} catch (err) {
|
||||
console.warn('HITL 旧会话等待时限迁移失败,将在下次加载时重试:', err);
|
||||
}
|
||||
} else if (normalizeHitlTimeoutSeconds(merged.timeoutSeconds, 0) > 0) {
|
||||
markLegacyHitlTimeoutMigrated(conversationId);
|
||||
}
|
||||
const uiMode = hitlEffectiveEnabled(merged) ? hitlModeNormalize(merged.mode) : 'off';
|
||||
const rawArr = Array.isArray(merged.sensitiveTools)
|
||||
? merged.sensitiveTools
|
||||
@@ -590,7 +592,10 @@ async function syncHitlConfigFromServer(conversationId) {
|
||||
localStorage.setItem('chat_hitl_config_' + conversationId, JSON.stringify(normalizedCfg));
|
||||
} catch (e) {}
|
||||
}
|
||||
if (typeof window.applyHitlConfigToUI === 'function') {
|
||||
if (
|
||||
getCurrentConversationIdForHitl() === conversationId &&
|
||||
typeof window.applyHitlConfigToUI === 'function'
|
||||
) {
|
||||
window.applyHitlConfigToUI(normalizedCfg);
|
||||
}
|
||||
reconcileHitlUiState();
|
||||
@@ -835,7 +840,11 @@ async function refreshHitlPending() {
|
||||
throw new Error('request failed');
|
||||
}
|
||||
const data = await resp.json();
|
||||
const items = Array.isArray(data.items) ? data.items : [];
|
||||
const rawItems = Array.isArray(data.items) ? data.items : [];
|
||||
const items = rawItems.filter(function (item) {
|
||||
return hitlReviewerNormalize(item && (item.reviewer || item.decidedBy || item.decided_by)) !== 'audit_agent' &&
|
||||
String(item && item.status || '').trim().toLowerCase() !== 'audit_running';
|
||||
});
|
||||
let workflowRuns = [];
|
||||
try {
|
||||
const wfResp = await hitlApiFetch('/api/workflows/runs/pending', { credentials: 'same-origin' });
|
||||
@@ -856,7 +865,8 @@ async function refreshHitlPending() {
|
||||
return conv.indexOf(searchQ) >= 0 || wfId.indexOf(searchQ) >= 0 || runId.indexOf(searchQ) >= 0 || label.indexOf(searchQ) >= 0;
|
||||
});
|
||||
}
|
||||
hitlPendingTotal = (typeof data.total === 'number' ? data.total : items.length) + workflowRuns.length;
|
||||
const hiddenAgentItems = rawItems.length - items.length;
|
||||
hitlPendingTotal = Math.max(0, (typeof data.total === 'number' ? data.total : rawItems.length) - hiddenAgentItems) + workflowRuns.length;
|
||||
const maxPage = Math.max(1, Math.ceil(hitlPendingTotal / hitlPendingPageSize));
|
||||
if (hitlPendingPage > maxPage) {
|
||||
hitlPendingPage = maxPage;
|
||||
|
||||
Reference in New Issue
Block a user