Add files via upload

This commit is contained in:
公明
2026-07-10 16:37:14 +08:00
committed by GitHub
parent 46f891a114
commit 04efc50161
12 changed files with 5208 additions and 81 deletions
+207 -10
View File
@@ -1,6 +1,10 @@
const AUTH_STORAGE_KEY = 'cyberstrike-auth';
let authToken = null;
let authTokenExpiry = null;
let authUser = null;
let authRoles = [];
let authPermissions = new Set();
let authScope = '';
let authPromise = null;
let authPromiseResolvers = [];
let isAppInitialized = false;
@@ -9,28 +13,42 @@ function isTokenValid() {
return !!authToken && authTokenExpiry instanceof Date && authTokenExpiry.getTime() > Date.now();
}
function saveAuth(token, expiresAt) {
function saveAuth(token, expiresAt, meta = {}) {
const expiry = expiresAt instanceof Date ? expiresAt : new Date(expiresAt);
authToken = token;
authTokenExpiry = expiry;
authUser = meta.user || null;
authRoles = Array.isArray(meta.roles) ? meta.roles : [];
authPermissions = new Set(Array.isArray(meta.permissions) ? meta.permissions : []);
authScope = meta.scope || '';
try {
localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify({
token,
expiresAt: expiry.toISOString(),
user: authUser,
roles: authRoles,
permissions: Array.from(authPermissions),
scope: authScope,
}));
} catch (error) {
console.warn('无法持久化认证信息:', error);
}
renderUserMenuProfile();
}
function clearAuthStorage() {
authToken = null;
authTokenExpiry = null;
authUser = null;
authRoles = [];
authPermissions = new Set();
authScope = '';
try {
localStorage.removeItem(AUTH_STORAGE_KEY);
} catch (error) {
console.warn('无法清除认证信息:', error);
}
renderUserMenuProfile();
}
function loadAuthFromStorage() {
@@ -51,6 +69,10 @@ function loadAuthFromStorage() {
}
authToken = stored.token;
authTokenExpiry = expiry;
authUser = stored.user || null;
authRoles = Array.isArray(stored.roles) ? stored.roles : [];
authPermissions = new Set(Array.isArray(stored.permissions) ? stored.permissions : []);
authScope = stored.scope || '';
return isTokenValid();
} catch (error) {
console.error('读取认证信息失败:', error);
@@ -68,6 +90,7 @@ function resolveAuthPromises(success) {
function showLoginOverlay(message = '') {
const overlay = document.getElementById('login-overlay');
const errorBox = document.getElementById('login-error');
const usernameInput = document.getElementById('login-username');
const passwordInput = document.getElementById('login-password');
if (!overlay) {
return;
@@ -83,7 +106,9 @@ function showLoginOverlay(message = '') {
}
}
setTimeout(function () {
if (passwordInput) {
if (usernameInput && !usernameInput.value) {
usernameInput.focus();
} else if (passwordInput) {
passwordInput.focus();
}
}, 100);
@@ -92,6 +117,7 @@ function showLoginOverlay(message = '') {
function hideLoginOverlay() {
const overlay = document.getElementById('login-overlay');
const errorBox = document.getElementById('login-error');
const usernameInput = document.getElementById('login-username');
const passwordInput = document.getElementById('login-password');
closeAppModal('login-overlay');
if (errorBox) {
@@ -101,6 +127,9 @@ function hideLoginOverlay() {
if (passwordInput) {
passwordInput.value = '';
}
if (usernameInput && !authUser) {
usernameInput.value = '';
}
}
function ensureAuthPromise() {
@@ -158,6 +187,13 @@ async function apiFetch(url, options = {}) {
: '未授权访问';
throw new Error(msg);
}
if (response.status === 403) {
const result = await response.clone().json().catch(() => ({}));
const msg = result.error || (typeof window !== 'undefined' && typeof window.t === 'function'
? window.t('auth.forbidden')
: '权限不足');
throw new Error(msg);
}
return response;
}
@@ -211,6 +247,7 @@ async function apiUploadWithProgress(url, formData, options = {}) {
async function submitLogin(event) {
event.preventDefault();
const usernameInput = document.getElementById('login-username');
const passwordInput = document.getElementById('login-password');
const errorBox = document.getElementById('login-error');
const submitBtn = document.querySelector('.login-submit');
@@ -219,6 +256,7 @@ async function submitLogin(event) {
return;
}
const username = usernameInput ? usernameInput.value.trim() : '';
const password = passwordInput.value.trim();
if (!password) {
if (errorBox) {
@@ -241,7 +279,7 @@ async function submitLogin(event) {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ password }),
body: JSON.stringify({ username, password }),
});
const result = await response.json().catch(() => ({}));
if (!response.ok || !result.token) {
@@ -255,8 +293,14 @@ async function submitLogin(event) {
return;
}
saveAuth(result.token, result.expires_at);
saveAuth(result.token, result.expires_at, {
user: result.user,
roles: result.roles,
permissions: result.permissions,
scope: result.scope,
});
hideLoginOverlay();
applyRBACToUI();
resolveAuthPromises(true);
if (!isAppInitialized) {
await bootstrapApp();
@@ -306,9 +350,151 @@ async function bootstrapApp() {
initializeChatUI();
isAppInitialized = true;
}
applyRBACToUI();
await refreshAppData();
}
const PAGE_PERMISSION_MAP = {
dashboard: 'dashboard:read',
chat: 'chat:read',
hitl: 'hitl:read',
'info-collect': 'fofa:execute',
tasks: 'tasks:read',
workflows: 'workflow:read',
projects: 'project:read',
vulnerabilities: 'vulnerability:read',
'chat-files': 'files:read',
webshell: 'webshell:read',
c2: 'c2:read',
'c2-listeners': 'c2:read',
'c2-sessions': 'c2:read',
'c2-tasks': 'c2:read',
'c2-payloads': 'c2:read',
'c2-events': 'c2:read',
'c2-profiles': 'c2:read',
mcp: 'mcp:read',
'mcp-monitor': 'monitor:read',
'mcp-management': 'mcp:read',
knowledge: 'knowledge:read',
'knowledge-retrieval-logs': 'knowledge:read',
'knowledge-management': 'knowledge:read',
skills: 'skills:read',
'skills-monitor': 'skills:read',
'skills-management': 'skills:read',
agents: 'agents:read',
'agents-management': 'agents:read',
roles: 'roles:read',
'roles-management': 'roles:read',
'platform-rbac': 'rbac:read',
settings: 'config:read',
};
function hasPermission(permission) {
return !permission || authPermissions.has(permission);
}
function applyRBACToUI() {
document.querySelectorAll('[data-page]').forEach((el) => {
const page = el.getAttribute('data-page');
const permission = PAGE_PERMISSION_MAP[page];
if (!permission) return;
const allowed = hasPermission(permission);
el.hidden = !allowed;
el.setAttribute('aria-hidden', allowed ? 'false' : 'true');
});
const userAvatar = document.querySelector('.user-avatar-btn');
if (userAvatar && authUser && authUser.username) {
const displayName = getAuthDisplayName();
userAvatar.setAttribute('title', displayName);
userAvatar.setAttribute('aria-label', authT('header.userMenuFor', '用户菜单:{{name}}', { name: displayName }));
}
renderUserMenuProfile();
}
function authT(key, fallback, opts = {}) {
if (typeof window !== 'undefined' && typeof window.t === 'function') {
const translated = window.t(key, opts);
if (translated && translated !== key) {
return translated;
}
}
return fallback.replace(/\{\{\s*(\w+)\s*\}\}/g, function (_, name) {
return Object.prototype.hasOwnProperty.call(opts, name) ? String(opts[name]) : '';
});
}
function getAuthDisplayName() {
if (!authUser) {
return authT('header.unknownUser', '未知用户');
}
return String(authUser.display_name || authUser.displayName || authUser.username || '').trim()
|| authT('header.unknownUser', '未知用户');
}
function getAuthUsername() {
if (!authUser) {
return '-';
}
const username = String(authUser.username || '').trim();
return username ? `@${username}` : '-';
}
function getScopeLabel(scope) {
const normalized = String(scope || '').trim().toLowerCase();
const keyMap = {
all: 'header.scopeAll',
assigned: 'header.scopeAssigned',
own: 'header.scopeOwn',
};
const fallbackMap = {
all: '全部资源',
assigned: '指定资源',
own: '自己的资源',
};
const key = keyMap[normalized] || 'header.scopeUnknown';
const fallback = fallbackMap[normalized] || '资源范围未知';
return authT(key, fallback);
}
function renderUserMenuProfile() {
const displayNameEl = document.getElementById('user-menu-display-name');
const usernameEl = document.getElementById('user-menu-username');
const scopeEl = document.getElementById('user-menu-scope');
const rolesEl = document.getElementById('user-menu-roles');
const permissionsEl = document.getElementById('user-menu-permissions');
const avatarBtn = document.getElementById('user-avatar-btn') || document.querySelector('.user-avatar-btn');
const displayName = getAuthDisplayName();
const roleCount = Array.isArray(authRoles) ? authRoles.length : 0;
const permissionCount = authPermissions instanceof Set ? authPermissions.size : 0;
if (displayNameEl) displayNameEl.textContent = displayName;
if (usernameEl) usernameEl.textContent = getAuthUsername();
if (scopeEl) scopeEl.textContent = getScopeLabel(authScope);
if (rolesEl) rolesEl.textContent = authT('header.rolesCount', '{{count}} 个角色', { count: roleCount });
if (permissionsEl) permissionsEl.textContent = authT('header.permissionsCount', '{{count}} 项权限', { count: permissionCount });
if (avatarBtn && authUser) {
avatarBtn.setAttribute('title', displayName);
avatarBtn.setAttribute('aria-label', authT('header.userMenuFor', '用户菜单:{{name}}', { name: displayName }));
} else if (avatarBtn) {
avatarBtn.setAttribute('aria-label', authT('header.userMenu', '用户菜单'));
}
}
function setUserMenuOpen(open) {
const dropdown = document.getElementById('user-menu-dropdown');
const avatarBtn = document.getElementById('user-avatar-btn') || document.querySelector('.user-avatar-btn');
if (!dropdown) return;
dropdown.style.display = open ? 'block' : 'none';
if (avatarBtn) {
avatarBtn.classList.toggle('active', open);
avatarBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
}
if (open) {
renderUserMenuProfile();
}
}
// 通用工具函数
function getStatusText(status) {
const s = (status && String(status).toLowerCase()) || '';
@@ -366,7 +552,15 @@ async function initializeApp() {
method: 'GET',
});
if (response.ok) {
const result = await response.json().catch(() => ({}));
saveAuth(result.token || authToken, result.expires_at || authTokenExpiry, {
user: result.user || authUser,
roles: result.roles || authRoles,
permissions: result.permissions || Array.from(authPermissions),
scope: result.scope || authScope,
});
hideLoginOverlay();
applyRBACToUI();
resolveAuthPromises(true);
await bootstrapApp();
return;
@@ -386,7 +580,7 @@ function toggleUserMenu() {
if (!dropdown) return;
const isVisible = dropdown.style.display !== 'none';
dropdown.style.display = isVisible ? 'none' : 'block';
setUserMenuOpen(!isVisible);
}
// 点击页面其他地方时关闭下拉菜单
@@ -397,17 +591,18 @@ document.addEventListener('click', function(event) {
if (dropdown && avatarBtn &&
!dropdown.contains(event.target) &&
!avatarBtn.contains(event.target)) {
dropdown.style.display = 'none';
setUserMenuOpen(false);
}
});
document.addEventListener('languagechange', function () {
renderUserMenuProfile();
});
// 退出登录
async function logout() {
// 关闭下拉菜单
const dropdown = document.getElementById('user-menu-dropdown');
if (dropdown) {
dropdown.style.display = 'none';
}
setUserMenuOpen(false);
try {
// 先尝试调用退出API(如果token有效)
@@ -434,5 +629,7 @@ async function logout() {
// 导出函数供HTML使用
window.toggleUserMenu = toggleUserMenu;
window.logout = logout;
window.hasPermission = hasPermission;
window.applyRBACToUI = applyRBACToUI;
document.addEventListener('DOMContentLoaded', initializeApp);
+118 -8
View File
@@ -2458,6 +2458,32 @@ function syncProcessDetailButtonLabels(messageId, expanded) {
});
}
/** 懒加载占位提示可点击,与工具栏「展开详情」行为一致 */
function bindProcessDetailsLazyHint(hostEl, messageId) {
if (!hostEl || !messageId) return;
const emptyEl = hostEl.classList && hostEl.classList.contains('progress-timeline-empty')
? hostEl
: hostEl.querySelector('.progress-timeline-empty');
if (!emptyEl || emptyEl.dataset.lazyHintBound === '1') return;
emptyEl.dataset.lazyHintBound = '1';
emptyEl.classList.add('progress-timeline-lazy-clickable');
emptyEl.setAttribute('role', 'button');
emptyEl.setAttribute('tabindex', '0');
const activate = () => {
if (typeof toggleProcessDetails === 'function') {
toggleProcessDetails(null, messageId);
}
};
emptyEl.addEventListener('click', activate);
emptyEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
activate();
}
});
}
window.bindProcessDetailsLazyHint = bindProcessDetailsLazyHint;
// 渲染过程详情
// options.append=true 时分页追加;options.markLoaded=false 时保留 lazy 标记(分页加载中)
function renderProcessDetails(messageId, processDetails, options) {
@@ -2465,6 +2491,13 @@ function renderProcessDetails(messageId, processDetails, options) {
const appendMode = !!renderOpts.append;
const prependMode = !!renderOpts.prepend;
const markLoaded = renderOpts.markLoaded !== false;
const toolStatusByProcessDetailId = new Map();
if (Array.isArray(renderOpts.toolExecutions)) {
renderOpts.toolExecutions.forEach((execution) => {
if (!execution || !execution.processDetailId) return;
toolStatusByProcessDetailId.set(String(execution.processDetailId), String(execution.status || '').toLowerCase());
});
}
const messageElement = document.getElementById(messageId);
if (!messageElement) {
return;
@@ -2529,6 +2562,7 @@ function renderProcessDetails(messageId, processDetails, options) {
const expandLabel = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
let lazyHint = expandLabel + '(点击后加载迭代详情)';
timeline.innerHTML = '<div class="progress-timeline-empty">' + lazyHint + '</div>';
bindProcessDetailsLazyHint(timeline, messageId);
timeline.classList.remove('expanded');
prefetchProcessDetailsSummaryHint(messageId, messageElement);
return;
@@ -2725,6 +2759,9 @@ function renderProcessDetails(messageId, processDetails, options) {
processDetailId: detail.id || '',
createdAt: detail.createdAt
};
if (eventType === 'tool_call' && detail.id && toolStatusByProcessDetailId.has(String(detail.id))) {
timelineOpts.toolStatus = toolStatusByProcessDetailId.get(String(detail.id));
}
if (eventType === 'tool_call' && data._mergedResult) {
timelineOpts.mergedResult = data._mergedResult;
}
@@ -2781,6 +2818,7 @@ function finishProcessDetailsRender(messageElement, processDetails, isLazyNotLoa
lazyHint.textContent = (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') +
'(点击后加载完整过程详情)';
timeline.appendChild(lazyHint);
bindProcessDetailsLazyHint(lazyHint, messageElement.id);
}
const hasPendingHitlInDetails = processDetails.some(d => d && d.eventType === 'hitl_interrupt');
@@ -2841,6 +2879,7 @@ function prefetchProcessDetailsSummaryHint(messageId, messageElement) {
const empty = timeline.querySelector('.progress-timeline-empty');
if (empty) {
empty.textContent = hint;
bindProcessDetailsLazyHint(timeline, messageId);
}
})
.catch(() => {});
@@ -2999,16 +3038,42 @@ function normalizeToolExecutionSummaryForButton(raw) {
};
}
function setPendingToolExecutionSummaries(messageElement, summaries) {
if (!messageElement || !messageElement.dataset || !Array.isArray(summaries)) return;
function cacheToolExecutionSummaries(messageElement, summaries) {
if (!messageElement || !messageElement.dataset || !Array.isArray(summaries)) return [];
const normalized = summaries
.map(normalizeToolExecutionSummaryForButton)
.filter((item) => item.toolName || item.executionId || item.toolCallId);
if (normalized.length > 0) {
messageElement.dataset.toolExecutionSummaries = JSON.stringify(normalized);
}
return normalized;
}
function getCachedToolExecutionSummaries(messageElement) {
if (!messageElement || !messageElement.dataset || !messageElement.dataset.toolExecutionSummaries) return [];
try {
const parsed = JSON.parse(messageElement.dataset.toolExecutionSummaries);
return Array.isArray(parsed) ? parsed.map(normalizeToolExecutionSummaryForButton) : [];
} catch (e) {
delete messageElement.dataset.toolExecutionSummaries;
return [];
}
}
function setPendingToolExecutionSummaries(messageElement, summaries) {
if (!messageElement || !messageElement.dataset || !Array.isArray(summaries)) return;
const normalized = cacheToolExecutionSummaries(messageElement, summaries);
if (normalized.length > 0) {
messageElement.dataset.pendingToolExecutionSummaries = JSON.stringify(normalized);
} else {
delete messageElement.dataset.pendingToolExecutionSummaries;
}
const renderedToolList = messageElement.querySelector('.mcp-tool-list');
if (normalized.length > 0 && renderedToolList && renderedToolList.querySelector('.mcp-detail-btn[data-exec-id], .mcp-detail-btn[data-tool-summary]')) {
appendMcpCallSummaryButtons(messageElement, normalized);
delete messageElement.dataset.pendingToolExecutionSummaries;
delete messageElement.dataset.pendingMcpExecutionIds;
}
if (typeof syncMcpToolsToggleButton === 'function') {
syncMcpToolsToggleButton(messageElement);
}
@@ -3202,6 +3267,34 @@ async function focusToolExecutionInProcessDetails(messageElement, summary, index
}, 2200);
}
async function resolveToolExecutionSummaryForFocus(messageElement, executionId, index) {
const wantedExecutionId = executionId == null ? '' : String(executionId).trim();
let summaries = getCachedToolExecutionSummaries(messageElement);
let item = wantedExecutionId
? summaries.find((summary) => summary.executionId === wantedExecutionId)
: summaries[index];
if (item && (item.processDetailId || item.toolCallId)) return item;
const backendId = messageElement && messageElement.dataset
? String(messageElement.dataset.backendMessageId || '').trim()
: '';
if (!backendId || typeof apiFetch !== 'function') return item || null;
try {
const res = await apiFetch('/api/messages/' + encodeURIComponent(backendId) + '/process-details?summary=1');
const payload = await res.json().catch(() => ({}));
if (!res.ok || !payload.summary || !Array.isArray(payload.summary.toolExecutions)) {
return item || null;
}
summaries = cacheToolExecutionSummaries(messageElement, payload.summary.toolExecutions);
item = wantedExecutionId
? summaries.find((summary) => summary.executionId === wantedExecutionId)
: summaries[index];
return item || null;
} catch (e) {
return item || null;
}
}
function toggleMcpToolList(assistantMessageId) {
const messageEl = document.getElementById(assistantMessageId);
if (!messageEl) return;
@@ -3255,7 +3348,14 @@ function appendMcpCallButtons(messageElement, executionIds) {
detailBtn.dataset.execId = execId;
detailBtn.dataset.execIndex = String(index + 1);
detailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.callNumber', { n: index + 1 }) : '调用 #' + (index + 1)) + '</span>';
detailBtn.onclick = () => showMCPDetail(execId);
detailBtn.onclick = async () => {
const summary = await resolveToolExecutionSummaryForFocus(messageElement, execId, index);
if (summary && (summary.processDetailId || summary.toolCallId)) {
await focusToolExecutionInProcessDetails(messageElement, summary, index);
return;
}
showMCPDetail(execId);
};
toolList.appendChild(detailBtn);
});
batchUpdateButtonToolNames(toolList, executionIds);
@@ -3273,10 +3373,16 @@ function appendMcpCallSummaryButtons(messageElement, summaries) {
const item = normalizeToolExecutionSummaryForButton(raw);
const key = item.executionId || item.toolCallId || `${item.toolName || 'tool'}-${index + 1}`;
const selector = '.mcp-detail-btn[data-tool-summary="' + CSS.escape(String(key)) + '"]';
if (toolList.querySelector(selector) || (item.executionId && toolList.querySelector('.mcp-detail-btn[data-exec-id="' + CSS.escape(String(item.executionId)) + '"]'))) {
const existingSummaryBtn = toolList.querySelector(selector);
const existingExecBtn = item.executionId
? toolList.querySelector('.mcp-detail-btn[data-exec-id="' + CSS.escape(String(item.executionId)) + '"]')
: null;
if (existingSummaryBtn) {
return;
}
const btn = document.createElement('button');
// 历史会话可能先按 executionId 渲染旧按钮,随后摘要才异步到达。
// 复用并升级该按钮,避免它永久保留“只看执行详情、不定位上下文”的旧处理器。
const btn = existingExecBtn || document.createElement('button');
btn.className = 'mcp-detail-btn';
btn.dataset.toolSummary = key;
if (item.executionId) {
@@ -3294,7 +3400,9 @@ function appendMcpCallSummaryButtons(messageElement, summaries) {
};
}
renderToolExecutionButtonContent(btn, item.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'), String(index + 1), item.status);
toolList.appendChild(btn);
if (!existingExecBtn) {
toolList.appendChild(btn);
}
});
syncMcpToolsToggleButton(messageElement);
}
@@ -3363,7 +3471,8 @@ function getToolExecutionStatusLabel(status) {
failed: 'mcpMonitor.statusFailed',
running: 'mcpMonitor.statusRunning',
cancelled: 'mcpMonitor.statusCancelled',
pending: 'mcpMonitor.statusPending'
pending: 'mcpMonitor.statusPending',
result_missing: 'timeline.resultMissing'
};
const key = keyMap[normalized];
if (key) {
@@ -3376,7 +3485,8 @@ function getToolExecutionStatusLabel(status) {
failed: '失败',
running: '运行中',
cancelled: '已取消',
pending: '等待中'
pending: '等待中',
result_missing: '结果记录缺失'
};
return fallback[normalized] || '';
}
+109 -41
View File
@@ -1357,6 +1357,10 @@ function integrateProgressToMCPSection(progressId, assistantMessageId, mcpExecut
</div>
</div>
`;
if (typeof window.bindProcessDetailsLazyHint === 'function') {
const tl = detailsContainer.querySelector('.progress-timeline');
if (tl) window.bindProcessDetailsLazyHint(tl, assistantMessageId);
}
}
const expandLabel = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
@@ -1492,11 +1496,13 @@ async function loadProcessDetailsPaginated(assistantMessageId, backendMessageId,
throw new Error((j && j.error) ? j.error : String(res.status));
}
const details = (j && Array.isArray(j.processDetails)) ? j.processDetails : [];
const toolExecutions = (j && Array.isArray(j.toolExecutions)) ? j.toolExecutions : [];
const hasMore = !!(j && j.hasMore);
renderProcessDetails(assistantMessageId, details, {
append: !isFirst || opts.append,
prepend: prepend,
markLoaded: autoLoadAll ? !hasMore : true
markLoaded: autoLoadAll ? !hasMore : true,
toolExecutions: toolExecutions
});
const responseOffset = j && typeof j.offset === 'number' ? j.offset : offset;
const total = j && typeof j.total === 'number' ? j.total : responseOffset + details.length;
@@ -1525,6 +1531,81 @@ async function loadProcessDetailsPaginated(assistantMessageId, backendMessageId,
window.loadProcessDetailsPaginated = loadProcessDetailsPaginated;
function resolveEventBackendMessageId(eventData) {
if (!eventData || typeof eventData !== 'object') return '';
const raw = eventData.messageId != null ? eventData.messageId : eventData.assistantMessageId;
return raw != null ? String(raw).trim() : '';
}
function triggerLazyProcessDetailsLoad(assistantMessageId, backendMessageId, detailsContainer) {
if (!assistantMessageId || !backendMessageId || !detailsContainer) return false;
if (detailsContainer.dataset.loading === '1') return false;
const collapseT = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情';
detailsContainer.dataset.loading = '1';
const timeline = detailsContainer.querySelector('.progress-timeline');
if (timeline) {
timeline.innerHTML = '<div class="progress-timeline-empty">' + ((typeof window.t === 'function') ? window.t('common.loading') : '加载中…') + '</div>';
}
loadProcessDetailsPaginated(assistantMessageId, backendMessageId, { autoLoadAll: false })
.catch((e) => {
console.error('加载过程详情失败:', e);
const tl = detailsContainer.querySelector('.progress-timeline');
if (tl) {
tl.innerHTML = '<div class="progress-timeline-empty">' + ((typeof window.t === 'function') ? window.t('chat.noProcessDetail') : '暂无过程详情(加载失败)') + '</div>';
if (typeof window.bindProcessDetailsLazyHint === 'function') {
window.bindProcessDetailsLazyHint(tl, assistantMessageId);
}
}
detailsContainer.dataset.lazyNotLoaded = '1';
detailsContainer.dataset.loaded = '0';
})
.finally(() => {
detailsContainer.dataset.loading = '0';
if (detailsContainer.dataset.userExpanded === '1') {
const tl = detailsContainer.querySelector('.progress-timeline');
if (tl) {
tl.classList.add('expanded');
}
if (typeof syncProcessDetailButtonLabels === 'function') {
syncProcessDetailButtonLabels(assistantMessageId, true);
} else {
document.querySelectorAll('#' + assistantMessageId + ' .process-detail-btn').forEach((btn) => {
btn.innerHTML = '<span>' + collapseT + '</span>';
});
}
}
});
return true;
}
function maybeReloadLazyProcessDetails(assistantMessageId) {
const detailsContainer = document.getElementById('process-details-' + assistantMessageId);
if (!detailsContainer) return;
const isLazy = detailsContainer.dataset.lazyNotLoaded === '1' && detailsContainer.dataset.loaded !== '1';
if (!isLazy) return;
const timeline = detailsContainer.querySelector('.progress-timeline');
const wantsExpanded = detailsContainer.dataset.userExpanded === '1' ||
!!(timeline && timeline.classList.contains('expanded'));
if (!wantsExpanded) return;
const messageEl = document.getElementById(assistantMessageId);
const backendId = messageEl && messageEl.dataset ? String(messageEl.dataset.backendMessageId || '').trim() : '';
if (!backendId) return;
triggerLazyProcessDetailsLoad(assistantMessageId, backendId, detailsContainer);
}
function scheduleProcessDetailsLoadWhenReady(assistantMessageId, detailsContainer, attempt) {
if (!assistantMessageId || !detailsContainer) return;
const tries = typeof attempt === 'number' ? attempt : 0;
if (tries > 25) return;
const messageEl = document.getElementById(assistantMessageId);
const backendId = messageEl && messageEl.dataset ? String(messageEl.dataset.backendMessageId || '').trim() : '';
if (backendId) {
triggerLazyProcessDetailsLoad(assistantMessageId, backendId, detailsContainer);
return;
}
setTimeout(() => scheduleProcessDetailsLoadWhenReady(assistantMessageId, detailsContainer, tries + 1), 200);
}
// 切换过程详情显示
function toggleProcessDetails(progressId, assistantMessageId) {
const detailsId = 'process-details-' + assistantMessageId;
@@ -1539,42 +1620,10 @@ function toggleProcessDetails(progressId, assistantMessageId) {
if (maybeLazy) {
const messageEl = document.getElementById(assistantMessageId);
const backendMessageId = messageEl && messageEl.dataset ? messageEl.dataset.backendMessageId : '';
if (backendMessageId && typeof apiFetch === 'function' && typeof renderProcessDetails === 'function') {
if (detailsContainer.dataset.loading === '1') {
// 正在加载中,避免重复请求
} else {
detailsContainer.dataset.loading = '1';
const timeline = detailsContainer.querySelector('.progress-timeline');
if (timeline) {
timeline.innerHTML = '<div class="progress-timeline-empty">' + ((typeof window.t === 'function') ? window.t('common.loading') : '加载中…') + '</div>';
}
loadProcessDetailsPaginated(assistantMessageId, backendMessageId, { autoLoadAll: false })
.catch((e) => {
console.error('加载过程详情失败:', e);
const tl = detailsContainer.querySelector('.progress-timeline');
if (tl) {
tl.innerHTML = '<div class="progress-timeline-empty">' + ((typeof window.t === 'function') ? window.t('chat.noProcessDetail') : '暂无过程详情(加载失败)') + '</div>';
}
detailsContainer.dataset.lazyNotLoaded = '1';
detailsContainer.dataset.loaded = '0';
})
.finally(() => {
detailsContainer.dataset.loading = '0';
if (detailsContainer.dataset.userExpanded === '1') {
const tl = detailsContainer.querySelector('.progress-timeline');
if (tl) {
tl.classList.add('expanded');
}
if (typeof syncProcessDetailButtonLabels === 'function') {
syncProcessDetailButtonLabels(assistantMessageId, true);
} else {
document.querySelectorAll('#' + assistantMessageId + ' .process-detail-btn').forEach((btn) => {
btn.innerHTML = '<span>' + collapseT + '</span>';
});
}
}
});
}
if (backendMessageId) {
triggerLazyProcessDetailsLoad(assistantMessageId, backendMessageId, detailsContainer);
} else {
scheduleProcessDetailsLoadWhenReady(assistantMessageId, detailsContainer, 0);
}
}
@@ -1647,6 +1696,7 @@ function applyBackendMessageIdToAssistantDom(domAssistantId, backendMessageId) {
if (typeof attachDeleteTurnButton === 'function') {
attachDeleteTurnButton(el);
}
maybeReloadLazyProcessDetails(domAssistantId);
}
/** 将后端用户消息 ID 绑定到最后一条尚未绑定 backendMessageId 的用户气泡 */
@@ -2480,7 +2530,7 @@ function handleStreamEvent(event, progressElement, progressId,
// 复用已有助手消息(若有),避免终态事件重复插入消息
{
const preferredMessageId = event.data && event.data.messageId ? event.data.messageId : null;
const preferredMessageId = resolveEventBackendMessageId(event.data) || null;
const { assistantId, assistantElement } = upsertTerminalAssistantMessage(event.message, preferredMessageId);
if (assistantId && preferredMessageId) {
applyBackendMessageIdToAssistantDom(assistantId, preferredMessageId);
@@ -2489,6 +2539,8 @@ function handleStreamEvent(event, progressElement, progressId,
const detailsId = 'process-details-' + assistantId;
if (!document.getElementById(detailsId)) {
integrateProgressToMCPSection(progressId, assistantId, typeof getMcpIds === 'function' ? (getMcpIds() || []) : []);
} else if (preferredMessageId) {
maybeReloadLazyProcessDetails(assistantId);
}
setTimeout(() => {
collapseAllProgressDetails(assistantId, progressId);
@@ -2501,7 +2553,7 @@ function handleStreamEvent(event, progressElement, progressId,
// Close any remaining running tool calls for this progress.
finalizeOutstandingToolCallsForProgress(progressId, 'failed');
break;
case 'response_start': {
const responseTaskState = progressTaskState.get(progressId);
const responseOriginalConversationId = responseTaskState?.conversationId;
@@ -2721,7 +2773,7 @@ function handleStreamEvent(event, progressElement, progressId,
// 复用已有助手消息(若有),避免终态事件重复插入消息
{
const preferredMessageId = event.data && event.data.messageId ? event.data.messageId : null;
const preferredMessageId = resolveEventBackendMessageId(event.data) || null;
const { assistantId, assistantElement } = upsertTerminalAssistantMessage(event.message, preferredMessageId);
if (assistantId && preferredMessageId) {
applyBackendMessageIdToAssistantDom(assistantId, preferredMessageId);
@@ -2730,6 +2782,8 @@ function handleStreamEvent(event, progressElement, progressId,
const detailsId = 'process-details-' + assistantId;
if (!document.getElementById(detailsId)) {
integrateProgressToMCPSection(progressId, assistantId, typeof getMcpIds === 'function' ? (getMcpIds() || []) : []);
} else if (preferredMessageId) {
maybeReloadLazyProcessDetails(assistantId);
}
setTimeout(() => {
collapseAllProgressDetails(assistantId, progressId);
@@ -3961,6 +4015,7 @@ function addTimelineItem(timeline, type, options) {
item.dataset.toolCallId = String(d.toolCallId).trim();
}
const merged = options.mergedResult || d._mergedResult;
const terminalStatus = String(options.toolStatus || '').toLowerCase();
if (merged) {
item.dataset.toolResultMerged = '1';
item.dataset.toolSuccess = merged.success !== false ? '1' : '0';
@@ -3968,6 +4023,12 @@ function addTimelineItem(timeline, type, options) {
if (d._mergedResultDetailId) {
item.dataset.toolResultDetailId = String(d._mergedResultDetailId);
}
} else if (terminalStatus === 'completed' || terminalStatus === 'failed') {
item.dataset.toolSuccess = terminalStatus === 'completed' ? '1' : '0';
item.classList.add(terminalStatus === 'completed' ? 'tool-call-completed' : 'tool-call-failed');
} else if (terminalStatus === 'result_missing') {
item.classList.add('tool-call-incomplete');
item.title = typeof window.t === 'function' ? window.t('timeline.resultMissing') : '结果记录缺失';
}
}
if (type === 'hitl_interrupt' && options.data && options.data.interruptId != null && String(options.data.interruptId).trim() !== '') {
@@ -4041,19 +4102,26 @@ function addTimelineItem(timeline, type, options) {
const data = options.data;
const args = parseToolCallArgsFromData(data);
const merged = options.mergedResult || data._mergedResult;
const terminalStatus = String(options.toolStatus || '').toLowerCase();
const hasTerminalStatus = terminalStatus === 'completed' || terminalStatus === 'failed';
const hasHistoricalStatus = hasTerminalStatus || terminalStatus === 'result_missing';
if (merged) {
if (merged.success !== false) {
item.classList.add('tool-call-completed');
} else {
item.classList.add('tool-call-failed');
}
} else if (hasTerminalStatus) {
item.classList.add(terminalStatus === 'completed' ? 'tool-call-completed' : 'tool-call-failed');
} else if (terminalStatus === 'result_missing') {
item.classList.add('tool-call-incomplete');
} else if (!options.skipPendingResult) {
item.classList.add('tool-call-running');
}
setToolCallDetailState(item, {
args: args,
resultData: merged || null,
pending: !merged && !options.skipPendingResult,
pending: !merged && !hasHistoricalStatus && !options.skipPendingResult,
processDetailId: options.processDetailId || '',
resultDetailId: data._mergedResultDetailId || '',
payloadDeferred: data._payloadDeferred === true || (merged && merged._payloadDeferred === true),
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -81,7 +81,7 @@ function initRouter() {
const hashParts = hash.split('?');
let pageId = hashParts[0];
if (pageId === 'c2') pageId = 'c2-listeners';
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'workflows', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'tasks', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'workflows', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'tasks', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
switchPage(pageId);
if (pageId === 'chat') {
scheduleChatConversationFromHash(500);
@@ -472,6 +472,11 @@ async function initPage(pageId) {
});
}
break;
case 'platform-rbac':
if (typeof initPlatformRbacPage === 'function') {
initPlatformRbacPage();
}
break;
case 'workflows':
if (typeof refreshWorkflows === 'function') {
refreshWorkflows();
@@ -538,7 +543,7 @@ document.addEventListener('DOMContentLoaded', function() {
let pageId = hashParts[0];
if (pageId === 'c2') pageId = 'c2-listeners';
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'tasks', 'workflows', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'tasks', 'workflows', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
switchPage(pageId);
if (pageId === 'chat') {
scheduleChatConversationFromHash(200);
+10
View File
@@ -509,6 +509,13 @@ function syncC2NavFromConfig(cfg) {
// 切换设置分类
function switchSettingsSection(section) {
if (section === 'rbac') {
if (typeof switchPage === 'function') {
switchPage('platform-rbac');
}
return;
}
// 更新导航项状态
document.querySelectorAll('.settings-nav-item').forEach(item => {
item.classList.remove('active');
@@ -3660,3 +3667,6 @@ document.addEventListener('languagechange', function () {
console.warn('languagechange MCP refresh failed', e);
}
});
window.initSettingsCustomSelects = initSettingsCustomSelects;
window.refreshSettingsCustomSelects = refreshSettingsCustomSelects;
+2
View File
@@ -1048,6 +1048,7 @@ function probeWebshellConnection(conn) {
cmd_param: conn.cmdParam || '',
encoding: webshellConnEncoding(conn),
os: webshellConnOS(conn),
connection_id: conn.id || '',
command: buildWebshellProbeCommand(probeToken)
})
})
@@ -3854,6 +3855,7 @@ function execWebshellCommand(conn, command) {
cmd_param: conn.cmdParam || '',
encoding: webshellConnEncoding(conn),
os: webshellConnOS(conn),
connection_id: conn.id || '',
command: command
})
}).then(function (r) { return r.json(); })