Add files via upload

This commit is contained in:
公明
2026-07-10 19:46:14 +08:00
committed by GitHub
parent 46a9b42fde
commit 823fb47a81
21 changed files with 717 additions and 212 deletions
+8
View File
@@ -19039,6 +19039,14 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
.rbac-workspace-view[hidden] { display: none !important; }
#rbac-role-save-btn[hidden] { display: none !important; }
/* display:flex 等会覆盖 [hidden] 默认 display:none,权限控件须强制隐藏 */
[data-require-permission][hidden],
[data-require-permission-any][hidden],
[data-require-permission].rbac-permission-denied,
[data-require-permission-any].rbac-permission-denied {
display: none !important;
}
.rbac-user-row.is-active { box-shadow: inset 3px 0 0 var(--primary-color, #2563eb); }
.rbac-role-card.is-selected {
border-color: rgba(37, 99, 235, 0.4);
+3
View File
@@ -458,6 +458,7 @@
"noFactsForVulnerability": "This vulnerability has no related facts yet. Link vulnerability or generate vulnerability draft from fact detail.",
"promptChooseFactByIndex": "This vulnerability is linked to {{count}} facts. Enter index to view:\n{{lines}}",
"enterProjectName": "Please enter project name",
"writePermissionDenied": "This account is read-only and cannot create or edit projects",
"saveFailed": "Save failed",
"invalidJson": "Invalid JSON format",
"scopeNoteAuthorizedWebOnly": "Authorized for Web application layer testing only",
@@ -1417,6 +1418,8 @@
"auth": {
"sessionExpired": "Session expired, please sign in again",
"unauthorized": "Unauthorized",
"forbidden": "Insufficient permissions",
"requestFailed": "Request failed",
"enterPassword": "Please enter password",
"loginFailedCheck": "Sign-in failed, please check the username or password",
"loginFailedRetry": "Sign-in failed, please try again later",
+3
View File
@@ -446,6 +446,7 @@
"noFactsForVulnerability": "该漏洞暂无关联事实,可在事实详情中「关联漏洞」或「生成漏洞草稿」建立链接",
"promptChooseFactByIndex": "该漏洞关联 {{count}} 条事实,输入序号查看:\n{{lines}}",
"enterProjectName": "请输入项目名称",
"writePermissionDenied": "当前账号仅有只读权限,无法创建或修改项目",
"saveFailed": "保存失败",
"invalidJson": "JSON 格式无效",
"scopeNoteAuthorizedWebOnly": "仅授权 Web 应用层测试",
@@ -1405,6 +1406,8 @@
"auth": {
"sessionExpired": "认证已过期,请重新登录",
"unauthorized": "未授权访问",
"forbidden": "权限不足",
"requestFailed": "请求失败",
"enterPassword": "请输入密码",
"loginFailedCheck": "登录失败,请检查用户名或密码",
"loginFailedRetry": "登录失败,请稍后重试",
+2
View File
@@ -86,6 +86,7 @@ async function loadMarkdownAgents() {
}
function showAddMarkdownAgentModal() {
if (typeof requirePermission === 'function' && !requirePermission('agents:write')) return;
markdownAgentsEditingFilename = null;
markdownAgentsEditingIsOrchestrator = false;
const modal = document.getElementById('agent-md-modal');
@@ -157,6 +158,7 @@ function parseToolsInput(s) {
}
async function saveMarkdownAgent() {
if (typeof requirePermission === 'function' && !requirePermission('agents:write')) return;
const name = document.getElementById('agent-md-name').value.trim();
if (!name) {
showNotification(_agentsT('agentsPage.nameRequired'), 'error');
+120 -8
View File
@@ -187,13 +187,7 @@ 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);
}
// 403 属于可预期的 RBAC 拒绝,返回 Response 供调用方通过 res.ok / ensureApiOk 处理。
return response;
}
@@ -351,6 +345,9 @@ async function bootstrapApp() {
isAppInitialized = true;
}
applyRBACToUI();
if (typeof installWriteHandlerGuards === 'function') {
installWriteHandlerGuards();
}
await refreshAppData();
}
@@ -393,7 +390,103 @@ function hasPermission(permission) {
return !permission || authPermissions.has(permission);
}
function applyRBACToUI() {
function hasAnyPermission(permissions) {
if (!Array.isArray(permissions) || !permissions.length) return true;
return permissions.some((permission) => hasPermission(permission));
}
async function readApiError(response, fallback) {
if (!response) {
return fallback || authT('auth.requestFailed', '请求失败');
}
try {
const body = await response.clone().json();
return body.error || body.message || fallback || authT('auth.requestFailed', '请求失败');
} catch (error) {
return fallback || authT('auth.requestFailed', '请求失败');
}
}
function notifyApiError(message, type = 'error') {
const text = (message || '').trim() || authT('auth.requestFailed', '请求失败');
if (typeof showNotification === 'function') {
showNotification(text, type);
return;
}
if (typeof showToast === 'function') {
showToast(text, type);
return;
}
alert(text);
}
async function notifyApiResponseError(response, fallback) {
notifyApiError(await readApiError(response, fallback));
}
async function ensureApiOk(response, fallback) {
if (response && response.ok) return true;
await notifyApiResponseError(response, fallback);
return false;
}
function requirePermission(permission, customMessage) {
const allowed = Array.isArray(permission)
? hasAnyPermission(permission)
: hasPermission(permission);
if (allowed) return true;
notifyApiError(customMessage || authT('auth.forbidden', '权限不足'));
return false;
}
function permissionAllowedForElement(el) {
if (!el) return true;
const anyOf = el.getAttribute('data-require-permission-any');
const permission = el.getAttribute('data-require-permission');
if (anyOf) {
return hasAnyPermission(anyOf.split(/[\s,|]+/).map((item) => item.trim()).filter(Boolean));
}
if (permission) {
return hasPermission(permission);
}
return true;
}
function applyPermissionElement(el) {
const anyOf = el.getAttribute('data-require-permission-any');
const permission = el.getAttribute('data-require-permission');
if (!anyOf && !permission) return;
const allowed = permissionAllowedForElement(el);
el.hidden = !allowed;
el.classList.toggle('rbac-permission-denied', !allowed);
if ('disabled' in el) {
el.disabled = !allowed;
}
el.setAttribute('aria-hidden', allowed ? 'false' : 'true');
el.setAttribute('aria-disabled', allowed ? 'false' : 'true');
}
let permissionClickGuardInstalled = false;
function installPermissionClickGuard() {
if (permissionClickGuardInstalled) return;
permissionClickGuardInstalled = true;
document.addEventListener('click', (event) => {
const target = event.target instanceof Element
? event.target.closest('[data-require-permission], [data-require-permission-any]')
: null;
if (!target || permissionAllowedForElement(target)) return;
event.preventDefault();
event.stopPropagation();
if (typeof event.stopImmediatePropagation === 'function') {
event.stopImmediatePropagation();
}
notifyApiError(authT('auth.forbidden', '权限不足'));
}, true);
}
function applyRBACToUI(root) {
installPermissionClickGuard();
document.querySelectorAll('[data-page]').forEach((el) => {
const page = el.getAttribute('data-page');
const permission = PAGE_PERMISSION_MAP[page];
@@ -402,6 +495,11 @@ function applyRBACToUI() {
el.hidden = !allowed;
el.setAttribute('aria-hidden', allowed ? 'false' : 'true');
});
const permissionRoot = root instanceof Element ? root : document;
permissionRoot.querySelectorAll('[data-require-permission], [data-require-permission-any]').forEach(applyPermissionElement);
if (permissionRoot instanceof Element && permissionRoot.matches('[data-require-permission], [data-require-permission-any]')) {
applyPermissionElement(permissionRoot);
}
const userAvatar = document.querySelector('.user-avatar-btn');
if (userAvatar && authUser && authUser.username) {
const displayName = getAuthDisplayName();
@@ -537,6 +635,7 @@ function formatMarkdown(text, options) {
}
function setupLoginUI() {
installPermissionClickGuard();
const loginForm = document.getElementById('login-form');
if (loginForm) {
loginForm.addEventListener('submit', submitLogin);
@@ -630,6 +729,19 @@ async function logout() {
window.toggleUserMenu = toggleUserMenu;
window.logout = logout;
window.hasPermission = hasPermission;
window.hasAnyPermission = hasAnyPermission;
window.readApiError = readApiError;
window.notifyApiError = notifyApiError;
window.notifyApiResponseError = notifyApiResponseError;
window.ensureApiOk = ensureApiOk;
window.requirePermission = requirePermission;
window.permissionAllowedForElement = permissionAllowedForElement;
window.applyRBACToUI = applyRBACToUI;
function rbacAfterDynamicRender(root) {
applyRBACToUI(root);
}
window.rbacAfterDynamicRender = rbacAfterDynamicRender;
document.addEventListener('DOMContentLoaded', initializeApp);
+16 -11
View File
@@ -647,7 +647,7 @@
</svg>
<h3 style="margin-bottom:8px;font-size:18px;font-weight:700;">${escapeHtml(c2t('c2.listeners.emptyTitle'))}</h3>
<p style="font-size:14px;">${escapeHtml(c2t('c2.listeners.emptyHint'))}</p>
<button class="btn-primary" onclick="C2.showCreateListenerModal()" style="margin-top:20px;">
<button class="btn-primary" data-require-permission="c2:write" onclick="C2.showCreateListenerModal()" style="margin-top:20px;">
${escapeHtml(c2t('c2.listeners.headerCreateBtn'))}
</button>
</div>`;
@@ -700,14 +700,15 @@
</div>
<div class="c2-listener-card-actions">
${l.status === 'stopped'
? `<button type="button" class="btn-primary btn-sm" onclick="C2.startListener('${l.id}')">▶ ${escapeHtml(c2t('c2.listeners.start'))}</button>`
: `<button type="button" class="btn-secondary btn-sm" onclick="C2.stopListener('${l.id}')">⏹ ${escapeHtml(c2t('c2.listeners.stop'))}</button>`
? `<button type="button" class="btn-primary btn-sm" data-require-permission="c2:write" onclick="C2.startListener('${l.id}')">▶ ${escapeHtml(c2t('c2.listeners.start'))}</button>`
: `<button type="button" class="btn-secondary btn-sm" data-require-permission="c2:write" onclick="C2.stopListener('${l.id}')">⏹ ${escapeHtml(c2t('c2.listeners.stop'))}</button>`
}
<button type="button" class="btn-secondary btn-sm" onclick="C2.editListener('${l.id}')">${escapeHtml(c2t('c2.listeners.edit'))}</button>
<button type="button" class="btn-danger btn-sm" onclick="C2.deleteListener('${l.id}')">${escapeHtml(c2t('c2.listeners.delete'))}</button>
<button type="button" class="btn-secondary btn-sm" data-require-permission="c2:write" onclick="C2.editListener('${l.id}')">${escapeHtml(c2t('c2.listeners.edit'))}</button>
<button type="button" class="btn-danger btn-sm" data-require-permission="c2:delete" onclick="C2.deleteListener('${l.id}')">${escapeHtml(c2t('c2.listeners.delete'))}</button>
</div>
</article>`;
}).join('');
if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(container);
};
C2.getListenerCallbackHost = function(l) {
@@ -1212,7 +1213,7 @@
</div>
<div class="c2-session-item-footer">
<span class="c2-session-meta c2-session-item-time" title="${escapeHtml(formatTime(s.lastCheckIn))}">${escapeHtml(formatRelativeTime(s.lastCheckIn) || formatTime(s.lastCheckIn))}</span>
<button type="button" class="c2-session-card-delete" onclick="event.stopPropagation(); C2.deleteSessionRecord('${s.id}');">${escapeHtml(c2t('c2.sessions.cardDeleteSession'))}</button>
<button type="button" class="c2-session-card-delete" data-require-permission="c2:delete" onclick="event.stopPropagation(); C2.deleteSessionRecord('${s.id}');">${escapeHtml(c2t('c2.sessions.cardDeleteSession'))}</button>
</div>
</div>
</div>
@@ -1225,6 +1226,7 @@
C2.selectSession(C2.sessions[0].id);
}
C2.syncSessionsToolbar();
if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(list);
};
C2.selectSession = function(id) {
@@ -1280,8 +1282,8 @@
<span class="c2-session-hero__heartbeat-value">${escapeHtml(heartbeatRel)}</span>
</div>
<div class="c2-session-actions">
<button class="btn-secondary btn-sm" onclick="C2.setSessionSleep('${s.id}')">${escapeHtml(c2t('c2.sessions.btnSleep'))}</button>
<button class="btn-danger btn-sm" onclick="C2.killSession('${s.id}')">${escapeHtml(c2t('c2.sessions.kill'))}</button>
<button class="btn-secondary btn-sm" data-require-permission="c2:write" onclick="C2.setSessionSleep('${s.id}')">${escapeHtml(c2t('c2.sessions.btnSleep'))}</button>
<button class="btn-danger btn-sm" data-require-permission="c2:write" onclick="C2.killSession('${s.id}')">${escapeHtml(c2t('c2.sessions.kill'))}</button>
</div>
</div>
</div>
@@ -1307,7 +1309,7 @@
<div class="c2-file-toolbar">
<button class="btn-ghost btn-sm" onclick="C2.goToParentDirectory()">${escapeHtml(c2t('c2.files.parent'))}</button>
<button class="btn-ghost btn-sm" onclick="C2.refreshFiles()">${escapeHtml(c2t('c2.files.refresh'))}</button>
<button type="button" class="btn-ghost btn-sm" id="c2-file-upload-btn" onclick="C2.openFileUploadPicker()" title="${escapeHtml(c2t('c2.files.upload'))}">${escapeHtml(c2t('c2.files.upload'))}</button>
<button type="button" class="btn-ghost btn-sm" id="c2-file-upload-btn" data-require-permission="c2:write" onclick="C2.openFileUploadPicker()" title="${escapeHtml(c2t('c2.files.upload'))}">${escapeHtml(c2t('c2.files.upload'))}</button>
<input type="file" id="c2-file-upload-input" style="display:none" onchange="C2.onC2FileUploadPick(event)" />
<span id="c2-current-path" class="c2-path-breadcrumb">/</span>
</div>
@@ -1351,6 +1353,7 @@
});
requestAnimationFrame(function () { C2.fitTerminal(); });
}, 0);
if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(container);
};
C2.switchTab = function(tab) {
@@ -3513,13 +3516,14 @@
${formatTime(e.createdAt)} · ${escapeHtml(e.category || '')}${e.sessionId ? ' · ' + escapeHtml(String(e.sessionId).substring(0, 8)) : ''}
</div>
</div>
<button type="button" class="btn-secondary c2-event-row-delete" onclick="event.stopPropagation();C2.deleteEventById('${eid}')" title="${delTitle}" aria-label="${delTitle}">🗑</button>
<button type="button" class="btn-secondary c2-event-row-delete" data-require-permission="c2:delete" onclick="event.stopPropagation();C2.deleteEventById('${eid}')" title="${delTitle}" aria-label="${delTitle}">🗑</button>
</div>
`;
}).join('');
C2.syncEventsToolbar();
if (typeof applyTranslations === 'function') applyTranslations(container);
if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(container);
};
C2.connectEventStream = function() {
@@ -3624,7 +3628,7 @@
<div class="c2-profile-card">
<div class="c2-profile-header">
<h4>${escapeHtml(p.name)}</h4>
<button class="btn-danger btn-sm" onclick="C2.deleteProfile('${p.id}')">${escapeHtml(c2t('common.delete'))}</button>
<button class="btn-danger btn-sm" data-require-permission="c2:delete" onclick="C2.deleteProfile('${p.id}')">${escapeHtml(c2t('common.delete'))}</button>
</div>
<div class="c2-profile-info">
<div><strong>UA:</strong> ${escapeHtml(p.userAgent || defVal)}</div>
@@ -3633,6 +3637,7 @@
</div>
</div>
`).join('');
if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(container);
};
C2.showCreateProfileModal = function() {
+5
View File
@@ -9045,6 +9045,7 @@ function navigateToVulnerabilitiesForContextConversation() {
// 从上下文菜单删除对话
function deleteConversationFromContext() {
if (typeof requirePermission === 'function' && !requirePermission('chat:delete')) return;
const convId = contextMenuConversationId;
if (!convId) return;
@@ -9596,6 +9597,7 @@ async function finishBatchGroupChangeAfterRemove() {
// 删除选中的对话
async function deleteSelectedConversations() {
if (typeof requirePermission === 'function' && !requirePermission('chat:delete')) return;
const checkboxes = document.querySelectorAll('.batch-conversation-checkbox:checked');
if (checkboxes.length === 0) {
alert(typeof window.t === 'function' ? window.t('batchManageModal.confirmDeleteNone') : '请先选择要删除的对话');
@@ -9923,6 +9925,7 @@ document.addEventListener('click', function(event) {
// 创建分组
async function createGroup(event) {
if (typeof requirePermission === 'function' && !requirePermission('group:write')) return;
// 阻止事件冒泡
if (event) {
event.preventDefault();
@@ -10356,6 +10359,7 @@ async function editGroup() {
// 删除分组
async function deleteGroup() {
if (typeof requirePermission === 'function' && !requirePermission('group:delete')) return;
if (!currentGroupId) return;
const deleteConfirmMsg = typeof window.t === 'function' ? window.t('chat.deleteGroupConfirm') : '确定要删除此分组吗?分组中的对话不会被删除,但会从分组中移除。';
@@ -10514,6 +10518,7 @@ async function pinGroupFromContext() {
// 从上下文菜单删除分组
async function deleteGroupFromContext() {
if (typeof requirePermission === 'function' && !requirePermission('group:delete')) return;
const groupId = contextMenuGroupId;
if (!groupId) return;
+6 -2
View File
@@ -210,6 +210,7 @@ function renderKnowledgeItemsByCategories(categoriesWithItems) {
html += '</div>';
container.innerHTML = html;
if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(container);
}
// 渲染知识项列表(向后兼容,用于按项分页的旧代码)
@@ -260,6 +261,7 @@ function renderKnowledgeItems(items) {
html += '</div>';
container.innerHTML = html;
if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(container);
}
// 渲染分页控件(按分类分页)
@@ -350,13 +352,13 @@ function renderKnowledgeItemCard(item) {
<div class="knowledge-item-card-title-row">
<h4 class="knowledge-item-card-title" title="${escapeHtml(item.title)}">${escapeHtml(item.title)}</h4>
<div class="knowledge-item-card-actions">
<button class="knowledge-item-action-btn" onclick="editKnowledgeItem('${item.id}')" title="编辑">
<button class="knowledge-item-action-btn" data-require-permission="knowledge:write" onclick="editKnowledgeItem('${item.id}')" title="编辑">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button class="knowledge-item-action-btn knowledge-item-delete-btn" onclick="deleteKnowledgeItem('${item.id}')" title="删除">
<button class="knowledge-item-action-btn knowledge-item-delete-btn" data-require-permission="knowledge:delete" onclick="deleteKnowledgeItem('${item.id}')" title="删除">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -941,6 +943,7 @@ async function rebuildKnowledgeIndexFull() {
// 显示添加知识项模态框
function showAddKnowledgeItemModal() {
if (typeof requirePermission === 'function' && !requirePermission('knowledge:write')) return;
currentEditingItemId = null;
document.getElementById('knowledge-item-modal-title').textContent = '添加知识';
document.getElementById('knowledge-item-category').value = '';
@@ -979,6 +982,7 @@ async function editKnowledgeItem(id) {
// 保存知识项
async function saveKnowledgeItem() {
if (typeof requirePermission === 'function' && !requirePermission('knowledge:write')) return;
// 防止重复提交
if (isSavingKnowledgeItem) {
showNotification('正在保存中,请勿重复点击...', 'warning');
+3 -2
View File
@@ -6095,7 +6095,7 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
const rawExecId = exec.id || '';
const executionId = escapeHtml(rawExecId);
const terminateBtn = status === 'running'
? `<button type="button" class="btn-secondary btn-monitor-abort" onclick="cancelMCPToolExecution('${rawExecId.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}')">${escapeHtml(terminateLabel)}</button>`
? `<button type="button" class="btn-secondary btn-monitor-abort" data-require-permission="monitor:write" onclick="cancelMCPToolExecution('${rawExecId.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}')">${escapeHtml(terminateLabel)}</button>`
: '';
const jsExecId = rawExecId.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const isSelected = monitorState.selectedExecutions.has(rawExecId);
@@ -6112,7 +6112,7 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
<div class="monitor-execution-actions">
<button class="btn-secondary" onclick="showMCPDetail('${executionId}')">${escapeHtml(viewDetailLabel)}</button>
${terminateBtn}
<button class="btn-secondary btn-delete" onclick="deleteExecution('${executionId}')" title="${escapeHtml(deleteExecTitle)}">${escapeHtml(deleteLabel)}</button>
<button class="btn-secondary btn-delete" data-require-permission="monitor:delete" onclick="deleteExecution('${executionId}')" title="${escapeHtml(deleteExecTitle)}">${escapeHtml(deleteLabel)}</button>
</div>
</td>
</tr>
@@ -6167,6 +6167,7 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
// 更新批量操作状态
updateBatchActionsState();
if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(tableContainer);
}
// 渲染监控面板分页控件
+97 -61
View File
@@ -26,10 +26,29 @@ function tpFmt(key, fallback, opts) {
return text;
}
function tpFmt(key, fallback, opts) {
const text = tp(key, opts);
if (!text || text === key) return fallback;
return text;
function requireProjectWrite() {
if (typeof requirePermission !== 'function') return true;
return requirePermission(
'project:write',
tpFmt('projects.writePermissionDenied', '当前账号仅有只读权限,无法创建或修改项目'),
);
}
function requireProjectDelete() {
if (typeof requirePermission !== 'function') return true;
return requirePermission(
'project:delete',
tpFmt('projects.writePermissionDenied', '当前账号仅有只读权限,无法删除项目'),
);
}
async function notifyProjectApiFailure(response, fallbackKey, fallbackText) {
if (typeof ensureApiOk === 'function') {
return ensureApiOk(response, tpFmt(fallbackKey, fallbackText));
}
if (!response || response.ok) return true;
alert(tpFmt(fallbackKey, fallbackText));
return false;
}
const PROJECTS_FILTER_SELECT_HANDLERS = {
@@ -838,8 +857,10 @@ function renderProjectsSidebar() {
updateProjectsListCount();
const list = projectsCache;
if (!projectsCache.length) {
el.innerHTML =
`<div class="projects-empty">${escapeHtml(tp('projects.noProjects'))}<br><button type="button" class="btn-primary btn-small projects-empty-btn" onclick="showNewProjectModal()">${escapeHtml(tp('projects.newProject'))}</button></div>`;
const createBtn = (typeof hasPermission === 'function' && hasPermission('project:write'))
? `<button type="button" class="btn-primary btn-small projects-empty-btn" onclick="showNewProjectModal()">${escapeHtml(tp('projects.newProject'))}</button>`
: '';
el.innerHTML = `<div class="projects-empty">${escapeHtml(tp('projects.noProjects'))}${createBtn ? `<br>${createBtn}` : ''}</div>`;
updateProjectsDetailVisibility();
renderProjectsPagination();
return;
@@ -866,6 +887,7 @@ function renderProjectsSidebar() {
</div>`;
}).join('');
updateProjectsDetailVisibility();
if (typeof applyRBACToUI === 'function') applyRBACToUI(el);
}
function clampProjectDescription(text) {
@@ -1029,6 +1051,7 @@ function toggleProjectFactGraphConnectMode() {
async function handleGraphConnectNodePick(factKey) {
if (!factKey || String(factKey).startsWith('vuln:')) return;
if (!requireProjectWrite()) return;
if (!_graphConnectSource) {
_graphConnectSource = factKey;
if (typeof showNotification === 'function') {
@@ -1052,10 +1075,7 @@ async function handleGraphConnectNodePick(factKey) {
}),
});
_graphConnectSource = null;
if (!res.ok) {
const err = await res.json().catch(() => ({}));
return alert(err.error || tp('projects.graphConnectFailed'));
}
if (!(await notifyProjectApiFailure(res, 'projects.graphConnectFailed', '创建边失败'))) return;
if (typeof showNotification === 'function') showNotification(tp('projects.graphConnectSuccess'), 'success');
loadProjectFactGraph();
loadProjectFacts();
@@ -1270,16 +1290,14 @@ function focusProjectFactGraphEdge(edgeId) {
async function deleteProjectFactEdge(edgeId) {
if (!edgeId || !currentProjectId) return;
if (!requireProjectWrite()) return;
const edge = (_currentGraphData?.edges || []).find((e) => e.id === edgeId);
if (isSyntheticGraphEdge(edge)) return;
if (!confirm(tp('projects.confirmDeleteGraphEdge'))) return;
const res = await apiFetch(`/api/projects/${currentProjectId}/fact-edges/${encodeURIComponent(edgeId)}`, {
method: 'DELETE',
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
return alert(err.error || tp('projects.graphEdgeDeleteFailed'));
}
if (!(await notifyProjectApiFailure(res, 'projects.graphEdgeDeleteFailed', '删除边失败'))) return;
if (typeof showNotification === 'function') showNotification(tp('projects.graphEdgeDeleteSuccess'), 'success');
const keepKey = _selectedGraphFactKey;
await loadProjectFactGraph();
@@ -1439,15 +1457,13 @@ function openProjectConversation(conversationId) {
async function promoteConversationAttackChain(conversationId) {
if (!currentProjectId || !conversationId) return;
if (!requireProjectWrite()) return;
if (!confirm(tp('projects.confirmPromoteAttackChain'))) return;
const res = await apiFetch(
`/api/projects/${currentProjectId}/promote-attack-chain/${encodeURIComponent(conversationId)}`,
{ method: 'POST' },
);
if (!res.ok) {
const err = await res.json().catch(() => ({}));
return alert(err.error || tp('projects.promoteAttackChainFailed'));
}
if (!(await notifyProjectApiFailure(res, 'projects.promoteAttackChainFailed', '沉淀失败'))) return;
const data = await res.json();
if (typeof showNotification === 'function') {
showNotification(
@@ -1581,6 +1597,7 @@ async function linkFactToExistingVulnerability() {
async function createVulnerabilityFromCurrentFact() {
const f = _factDetailFact;
if (!f || !currentProjectId) return;
if (typeof requirePermission === 'function' && !requirePermission('vulnerability:write')) return;
let convId =
(f.source_conversation_id || '').trim() ||
(typeof window.currentConversationId === 'string' ? window.currentConversationId.trim() : '');
@@ -1611,12 +1628,9 @@ async function createVulnerabilityFromCurrentFact() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
return alert(err.error || tp('projects.createVulnerabilityFailed'));
}
if (!(await notifyProjectApiFailure(res, 'projects.createVulnerabilityFailed', '创建漏洞失败'))) return;
const vuln = await res.json();
await apiFetch(`/api/projects/${currentProjectId}/facts/${encodeURIComponent(f.id)}`, {
const upd = await apiFetch(`/api/projects/${currentProjectId}/facts/${encodeURIComponent(f.id)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -1628,6 +1642,7 @@ async function createVulnerabilityFromCurrentFact() {
related_vulnerability_id: vuln.id,
}),
});
if (!(await notifyProjectApiFailure(upd, 'projects.linkFailed', '关联失败'))) return;
const createdVulnLabel = vuln.title || vuln.id;
const successMsg = tp('projects.createVulnerabilityAndLinkSuccess', {
value: createdVulnLabel,
@@ -1648,6 +1663,7 @@ function inferSeverityFromFact(f) {
}
async function deprecateProjectFactByKey(factKey) {
if (!requireProjectWrite()) return;
if (!confirm(
tp('projects.confirmDeprecateFact', {
factKey,
@@ -1659,11 +1675,12 @@ async function deprecateProjectFactByKey(factKey) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fact_key: factKey }),
});
if (!res.ok) return alert(tp('projects.operationFailed'));
if (!(await notifyProjectApiFailure(res, 'projects.operationFailed', '操作失败'))) return;
loadProjectFacts();
}
async function restoreProjectFactByKey(factKey) {
if (!requireProjectWrite()) return;
if (!confirm(
tp('projects.confirmRestoreFact', {
factKey,
@@ -1675,10 +1692,7 @@ async function restoreProjectFactByKey(factKey) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fact_key: factKey, confidence: 'tentative' }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
return alert(err.error || tp('projects.operationFailed'));
}
if (!(await notifyProjectApiFailure(res, 'projects.operationFailed', '操作失败'))) return;
loadProjectFacts();
}
@@ -1793,6 +1807,7 @@ function closeProjectsOverlay(id) {
}
function showNewProjectModal() {
if (!requireProjectWrite()) return;
document.getElementById('project-modal-title').textContent = tp('projects.modalNewTitle');
const sub = document.getElementById('project-modal-subtitle');
if (sub) sub.textContent = tp('projects.modalNewSubtitle');
@@ -1806,6 +1821,7 @@ function showNewProjectModal() {
async function showEditProjectModal(projectId) {
if (!projectId) return;
if (!requireProjectWrite()) return;
window._projectModalFromChat = false;
window._projectModalEditId = projectId;
document.getElementById('project-modal-title').textContent = tp('projects.modalEditTitle');
@@ -1848,6 +1864,7 @@ function showNewProjectModalFromChat() {
}
async function saveProjectModal() {
if (!requireProjectWrite()) return;
const name = document.getElementById('project-modal-name').value.trim().slice(0, PROJECT_NAME_MAX_LENGTH);
if (!name) return alert(tp('projects.enterProjectName'));
const body = {
@@ -1855,31 +1872,39 @@ async function saveProjectModal() {
description: clampProjectDescription(document.getElementById('project-modal-description').value),
};
const editId = window._projectModalEditId;
const res = editId
? await apiFetch(`/api/projects/${editId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
: await apiFetch('/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (!res.ok) {
const err = await res.json().catch(() => ({}));
alert(err.error || tp('projects.saveFailed'));
return;
}
const fromChat = !!window._projectModalFromChat;
const fromWebshellConnId = window._projectModalFromWebshellConnId || '';
window._projectModalFromChat = false;
window._projectModalFromWebshellConnId = '';
closeProjectModal();
const saved = await res.json();
await loadProjectsList();
if (saved.id) {
if (fromWebshellConnId && !editId) {
if (typeof applyWebshellAiProjectSelection === 'function') {
await applyWebshellAiProjectSelection(saved.id);
const submitBtn = document.getElementById('project-modal-submit-btn');
if (submitBtn) submitBtn.disabled = true;
try {
const res = editId
? await apiFetch(`/api/projects/${editId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
: await apiFetch('/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (!(await notifyProjectApiFailure(res, 'projects.saveFailed', '保存失败'))) return;
const fromChat = !!window._projectModalFromChat;
const fromWebshellConnId = window._projectModalFromWebshellConnId || '';
window._projectModalFromChat = false;
window._projectModalFromWebshellConnId = '';
closeProjectModal();
const saved = await res.json();
await loadProjectsList();
if (saved.id) {
if (fromWebshellConnId && !editId) {
if (typeof applyWebshellAiProjectSelection === 'function') {
await applyWebshellAiProjectSelection(saved.id);
}
} else if (fromChat && !editId) {
await applyChatProjectSelection(saved.id);
} else {
await selectProject(saved.id);
}
} else if (fromChat && !editId) {
await applyChatProjectSelection(saved.id);
} else {
await selectProject(saved.id);
}
} catch (error) {
if (typeof notifyApiError === 'function') {
notifyApiError(error?.message || tpFmt('projects.saveFailed', '保存失败'));
} else {
alert(error?.message || tpFmt('projects.saveFailed', '保存失败'));
}
} finally {
if (submitBtn) submitBtn.disabled = false;
}
}
@@ -1914,7 +1939,7 @@ function insertProjectScopeExample() {
}
async function saveProjectSettings() {
if (!currentProjectId) return;
if (!currentProjectId || !requireProjectWrite()) return;
const scopeRaw = document.getElementById('project-edit-scope').value.trim();
if (scopeRaw) {
try {
@@ -1936,10 +1961,14 @@ async function saveProjectSettings() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) return alert(tp('projects.saveFailed'));
if (!(await notifyProjectApiFailure(res, 'projects.saveFailed', '保存失败'))) return;
await loadProjectsList();
await selectProject(currentProjectId);
alert(tp('projects.saved'));
if (typeof notifyApiError === 'function') {
notifyApiError(tp('projects.saved'), 'success');
} else {
alert(tp('projects.saved'));
}
}
function findProjectById(projectId) {
@@ -2002,6 +2031,7 @@ function showProjectListActionMenu(event, projectId) {
}
if (deleteText) deleteText.textContent = tp('projects.deleteProject');
positionProjectListActionMenu(event);
if (typeof applyRBACToUI === 'function') applyRBACToUI(menu);
}
function initProjectListActionMenu() {
@@ -2020,6 +2050,7 @@ function initProjectListActionMenu() {
}
async function toggleProjectArchiveById(projectId) {
if (!requireProjectWrite()) return;
const p = findProjectById(projectId);
if (!p) return;
const cur = p.status || 'active';
@@ -2030,7 +2061,7 @@ async function toggleProjectArchiveById(projectId) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: next }),
});
if (!res.ok) return alert(tp('projects.operationFailed'));
if (!(await notifyProjectApiFailure(res, 'projects.operationFailed', '操作失败'))) return;
await loadProjectsList();
if (currentProjectId === projectId && projectsCache.some((item) => item.id === projectId)) {
await selectProject(projectId);
@@ -2042,10 +2073,11 @@ async function toggleProjectArchiveById(projectId) {
}
async function deleteProjectById(projectId) {
if (!requireProjectDelete()) return;
if (!projectId || !confirm(tp('projects.confirmDeleteProject'))) return;
const deletedIndex = projectsCache.findIndex((p) => p.id === projectId);
const res = await apiFetch(`/api/projects/${projectId}`, { method: 'DELETE' });
if (!res.ok) return alert(tp('projects.deleteFailed'));
if (!(await notifyProjectApiFailure(res, 'projects.deleteFailed', '删除失败'))) return;
if (getActiveProjectId() === projectId) setActiveProjectId('');
if (currentProjectId === projectId) currentProjectId = null;
await loadProjectsList();
@@ -2147,12 +2179,14 @@ function fillFactModalForm(f) {
function showAddFactModal() {
if (!currentProjectId) return alert(tp('projects.selectProjectFirst'));
if (!requireProjectWrite()) return;
resetFactModalForm();
openProjectsOverlay('fact-modal');
}
async function showEditFactModal(factKey) {
if (!currentProjectId) return alert(tp('projects.selectProjectFirst'));
if (!requireProjectWrite()) return;
resetFactModalForm();
openProjectsOverlay('fact-modal', { focus: false });
const res = await apiFetch(
@@ -2175,6 +2209,7 @@ function closeFactModal() {
}
async function saveFactModal() {
if (!requireProjectWrite()) return;
const fact_key = document.getElementById('fact-modal-key').value.trim();
const summary = document.getElementById('fact-modal-summary').value.trim();
const category = document.getElementById('fact-modal-category').value.trim() || 'note';
@@ -2208,18 +2243,17 @@ async function saveFactModal() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
return alert(err.error || tp('projects.saveFailed'));
}
if (!(await notifyProjectApiFailure(res, 'projects.saveFailed', '保存失败'))) return;
closeFactModal();
loadProjectFacts();
if (currentProjectTab === 'graph') loadProjectFactGraph();
}
async function deleteProjectFact(id) {
if (!requireProjectWrite()) return;
if (!confirm(tp('projects.confirmDeleteFact'))) return;
await apiFetch(`/api/projects/${currentProjectId}/facts/${id}`, { method: 'DELETE' });
const res = await apiFetch(`/api/projects/${currentProjectId}/facts/${id}`, { method: 'DELETE' });
if (!(await notifyProjectApiFailure(res, 'projects.operationFailed', '操作失败'))) return;
loadProjectFacts();
if (currentProjectTab === 'graph') loadProjectFactGraph();
}
@@ -2511,6 +2545,8 @@ async function renderChatProjectPanel() {
});
clearProjectPickerPanelSearch('chat', 'chat-project-search');
await loadChatProjectPanelList();
const panel = document.getElementById('chat-project-panel');
if (panel && typeof applyRBACToUI === 'function') applyRBACToUI(panel);
requestAnimationFrame(() => document.getElementById('chat-project-search')?.focus());
}
+278
View File
@@ -0,0 +1,278 @@
/**
* 全局写操作权限守卫:为各页面 onclick 绑定的 window/C2 方法统一包一层 requirePermission。
* 在全部业务脚本加载完成后执行(见 index.html 引用顺序)。
*/
(function () {
'use strict';
const GLOBAL_WRITE_HANDLER_PERMISSIONS = {
// 对话 / 分组
sendMessage: 'chat:write',
startNewConversation: 'chat:write',
deleteConversation: 'chat:delete',
deleteConversationTurnFromUI: 'chat:delete',
deleteConversationFromContext: 'chat:delete',
showBatchManageModal: 'chat:delete',
deleteSelectedConversations: 'chat:delete',
renameConversation: 'chat:write',
pinConversation: 'chat:write',
showCreateGroupModal: 'group:write',
createGroup: 'group:write',
editGroup: 'group:write',
deleteGroup: 'group:delete',
deleteGroupFromContext: 'group:delete',
pinGroupFromContext: 'group:write',
renameGroupFromContext: 'group:write',
applyBatchGroupChange: 'group:write',
applyCustomIcon: 'group:write',
// 人机协同
applyHitlSidebarConfig: 'hitl:write',
saveHitlPageWhitelist: 'hitl:write',
saveHitlAuditStrategy: 'hitl:write',
saveHitlConversationConfig: 'hitl:write',
submitHitlDecision: 'hitl:write',
submitHitlDecisionWithPayload: 'hitl:write',
submitWorkflowHitlDecisionFromPage: 'hitl:write',
submitWorkflowHitlDecision: 'hitl:write',
dismissHitlItem: 'hitl:write',
batchDeleteHitlLogs: 'hitl:write',
clearHitlLogs: 'hitl:write',
// 项目
showNewProjectModal: 'project:write',
showNewProjectModalFromChat: 'project:write',
showNewProjectModalFromWebshellAi: 'project:write',
showEditProjectModal: 'project:write',
saveProjectModal: 'project:write',
saveProjectSettings: 'project:write',
archiveCurrentProject: 'project:write',
deleteCurrentProject: 'project:delete',
deleteProjectFromListMenu: 'project:delete',
toggleProjectFactGraphConnectMode: 'project:write',
editProjectFromListMenu: 'project:write',
toggleProjectArchiveFromListMenu: 'project:write',
showAddFactModal: 'project:write',
showEditFactModal: 'project:write',
editSelectedGraphFact: 'project:write',
editFactFromDetail: 'project:write',
saveFactModal: 'project:write',
deleteProjectFactEdge: 'project:delete',
deprecateProjectFactByKey: 'project:write',
restoreProjectFactByKey: 'project:write',
promoteConversationAttackChain: 'attackchain:write',
linkFactToExistingVulnerability: 'project:write',
createVulnerabilityFromCurrentFact: 'vulnerability:write',
unbindConversationFromProject: 'project:write',
// 漏洞
showAddVulnerabilityModal: 'vulnerability:write',
saveVulnerability: 'vulnerability:write',
deleteVulnerability: 'vulnerability:delete',
batchDeleteVulnerabilityReports: 'vulnerability:delete',
exportVulnerabilityReports: 'vulnerability:read',
changeVulnerabilityStatus: 'vulnerability:write',
bindVulnerabilityProject: 'vulnerability:write',
// 角色 / Skills / Agents
showAddRoleModal: 'roles:write',
saveRole: 'roles:write',
deleteRole: 'roles:delete',
showAddSkillModal: 'skills:write',
saveSkill: 'skills:write',
deleteSkill: 'skills:delete',
showAddMarkdownAgentModal: 'agents:write',
saveMarkdownAgent: 'agents:write',
deleteMarkdownAgent: 'agents:delete',
// 知识库
buildKnowledgeIndex: 'knowledge:write',
rebuildKnowledgeIndexFull: 'knowledge:write',
showAddKnowledgeItemModal: 'knowledge:write',
saveKnowledgeItem: 'knowledge:write',
editKnowledgeItem: 'knowledge:write',
deleteKnowledgeItem: 'knowledge:delete',
deleteRetrievalLog: 'knowledge:delete',
// 设置 / MCP
applySettings: 'config:write',
saveToolsConfig: 'config:write',
saveExternalMCP: 'mcp:write',
showAddExternalMCPModal: 'mcp:write',
deleteExternalMCP: 'mcp:write',
toggleExternalMCP: 'mcp:write',
changePassword: 'auth:self',
testOpenAIConnection: 'config:write',
testVisionConnection: 'config:write',
testHitlAuditModelConnection: 'config:write',
submitMcpToolAbortModal: 'monitor:write',
cancelMCPToolExecution: 'monitor:write',
// FOFA / 信息收集
submitFofaSearch: 'fofa:execute',
scanFofaRow: 'fofa:execute',
batchScanSelectedFofaRows: 'fofa:execute',
exportFofaResults: 'fofa:execute',
// 任务队列
showBatchImportModal: 'tasks:write',
deleteBatchQueue: 'tasks:delete',
deleteBatchQueueFromList: 'tasks:delete',
createBatchQueue: 'tasks:write',
saveAddBatchTask: 'tasks:write',
saveInlineTask: 'tasks:write',
deleteBatchTask: 'tasks:delete',
deleteBatchTaskFromElement: 'tasks:delete',
saveInlineTitle: 'tasks:write',
saveInlineRole: 'tasks:write',
saveInlineAgentMode: 'tasks:write',
saveInlineConcurrency: 'tasks:write',
saveInlineSchedule: 'tasks:write',
startBatchQueue: 'tasks:write',
pauseBatchQueue: 'tasks:write',
rerunBatchQueue: 'tasks:write',
runSingleBatchTask: 'tasks:write',
editBatchTaskFromElement: 'tasks:write',
batchCancelTasks: 'tasks:write',
cancelTask: 'tasks:write',
cancelActiveTask: 'tasks:write',
cancelProgressTask: 'tasks:write',
// 工作流
saveWorkflowDraft: 'workflow:write',
applyWorkflowMetaModal: 'workflow:write',
deleteCurrentWorkflow: 'workflow:delete',
deleteWorkflowSelection: 'workflow:delete',
dryRunWorkflowDraft: 'workflow:execute',
toggleWorkflowEnabled: 'workflow:write',
addWorkflowNodeFromPalette: 'workflow:write',
toggleWorkflowConnectMode: 'workflow:write',
addWorkflowCustomField: 'workflow:write',
// 文件管理
saveChatFilesEdit: 'files:write',
deleteChatFile: 'files:delete',
deleteChatFileIdx: 'files:delete',
deleteChatFolderFromBrowse: 'files:delete',
submitChatFilesRename: 'files:write',
submitChatFilesMkdir: 'files:write',
chatFilesOpenUploadPicker: 'files:write',
chatFilesUploadFiles: 'files:write',
onChatFilesUploadPick: 'files:write',
chatFilesUploadToFolderClick: 'files:write',
chatFilesDeleteFolderFromBtn: 'files:delete',
// 监控
deleteExecution: 'monitor:delete',
batchDeleteExecutions: 'monitor:delete',
// 攻击链
regenerateAttackChain: 'attackchain:write',
exportAttackChain: 'attackchain:read',
// 通知
markAllNotificationsSeen: 'notification:write',
// RBAC
saveRbacUser: 'rbac:write',
deleteSelectedRbacUser: 'rbac:write',
saveRbacRole: 'rbac:write',
deleteRbacRole: 'rbac:write',
createRbacAssignment: 'rbac:write',
deleteRbacAssignment: 'rbac:write',
saveSelectedUserRoles: 'rbac:write',
// WebShell
showAddWebshellModal: 'webshell:write',
showEditWebshellModal: 'webshell:write',
saveWebshellConnection: 'webshell:write',
deleteWebshell: 'webshell:delete',
testWebshellConnection: 'webshell:write',
// 机器人
openRobotCreateModal: 'robot:write',
openRobotEditor: 'robot:write',
startWechatRobotBind: 'robot:write',
submitWechatVerifyCode: 'robot:write',
// 审计
exportAuditLogs: 'audit:read',
exportAuditLogsCsv: 'audit:read',
runAuditExport: 'audit:read',
// Agent 中断
submitUserInterruptContinue: 'agent:execute',
submitUserInterruptHardCancel: 'agent:execute',
// 终端(多会话)
addTerminalTab: 'terminal:execute',
removeTerminalTab: 'terminal:execute',
};
const NAMESPACE_WRITE_HANDLER_PERMISSIONS = {
C2: {
showCreateListenerModal: 'c2:write',
createListener: 'c2:write',
saveListener: 'c2:write',
editListener: 'c2:write',
startListener: 'c2:write',
stopListener: 'c2:write',
deleteListener: 'c2:delete',
deleteSessionRecord: 'c2:delete',
deleteSelectedSessions: 'c2:delete',
deleteFilteredSessions: 'c2:delete',
killSession: 'c2:write',
setSessionSleep: 'c2:write',
submitSessionSleep: 'c2:write',
uploadFileToImplant: 'c2:write',
onC2FileUploadPick: 'c2:write',
openFileUploadPicker: 'c2:write',
deleteTaskById: 'c2:delete',
deleteSelectedTasks: 'c2:delete',
cancelTask: 'c2:write',
buildBeacon: 'c2:write',
generateOneliner: 'c2:write',
createProfile: 'c2:write',
showCreateProfileModal: 'c2:write',
deleteProfile: 'c2:delete',
deleteEventById: 'c2:delete',
deleteSelectedEvents: 'c2:delete',
runTerminalCommand: 'terminal:execute',
executeInTerminal: 'terminal:execute',
},
};
function wrapHandlerWithPermission(fn, permission) {
if (typeof fn !== 'function') return fn;
if (fn.__rbacGuarded) return fn;
const wrapped = function rbacGuardedHandler(...args) {
if (typeof requirePermission === 'function' && !requirePermission(permission)) {
return undefined;
}
return fn.apply(this, args);
};
wrapped.__rbacGuarded = true;
wrapped.__rbacOriginal = fn;
return wrapped;
}
function installWriteHandlerGuards() {
Object.entries(GLOBAL_WRITE_HANDLER_PERMISSIONS).forEach(([name, permission]) => {
if (typeof window[name] === 'function') {
window[name] = wrapHandlerWithPermission(window[name], permission);
}
});
Object.entries(NAMESPACE_WRITE_HANDLER_PERMISSIONS).forEach(([ns, methods]) => {
const root = window[ns];
if (!root || typeof root !== 'object') return;
Object.entries(methods).forEach(([method, permission]) => {
if (typeof root[method] === 'function') {
root[method] = wrapHandlerWithPermission(root[method], permission);
}
});
});
}
window.installWriteHandlerGuards = installWriteHandlerGuards;
installWriteHandlerGuards();
})();
+4
View File
@@ -209,6 +209,10 @@ function renderRbacPendingSelection() {
}
function rbacNotify(message, type = 'info') {
if (typeof notifyApiError === 'function') {
notifyApiError(message, type);
return;
}
if (typeof showNotification === 'function') showNotification(message, type);
else if (type === 'error') alert(message);
}
+2
View File
@@ -1279,6 +1279,7 @@ function setSelectedRoleTools(selectedToolKeys) {
// 显示添加角色模态框
async function showAddRoleModal() {
if (typeof requirePermission === 'function' && !requirePermission('roles:write')) return;
const modal = document.getElementById('role-modal');
if (!modal) return;
@@ -1627,6 +1628,7 @@ async function loadAllToolsToStateMap() {
// 保存角色
async function saveRole() {
if (typeof requirePermission === 'function' && !requirePermission('roles:write')) return;
const name = document.getElementById('role-name').value.trim();
if (!name) {
showNotification(_t('roleModal.roleNameRequired'), 'error');
+9 -3
View File
@@ -120,6 +120,10 @@ function switchPage(pageId) {
// 页面特定的初始化
initPage(pageId);
if (typeof applyRBACToUI === 'function') {
applyRBACToUI(targetPage);
}
}
}
window.switchPage = switchPage;
@@ -414,9 +418,11 @@ async function initPage(pageId) {
startExternalMcpPoll();
}
};
// 先拉取配置(含 tool_search 常驻列表),再加载工具与外部 MCP
if (typeof loadConfig === 'function') {
loadConfig(false)
// 先拉取配置(含 tool_search 常驻列表),再加载工具与外部 MCP
// 仅有 mcp:read 的用户无需拉全量 /api/config(需 config:read)。
const canLoadFullConfig = typeof hasPermission !== 'function' || hasPermission('config:read');
if (typeof loadConfig === 'function' && canLoadFullConfig) {
loadConfig(false, { silent: true })
.catch(err => {
console.warn('加载配置失败(将继续加载 MCP 列表):', err);
})
+28 -6
View File
@@ -584,10 +584,14 @@ window.onclick = function(event) {
}
// 加载配置
async function loadConfig(loadTools = true) {
async function loadConfig(loadTools = true, options = {}) {
const silent = options && options.silent === true;
try {
const response = await apiFetch('/api/config');
if (!response.ok) {
if (typeof readApiError === 'function') {
throw new Error(await readApiError(response, '获取配置失败'));
}
throw new Error('获取配置失败');
}
@@ -993,10 +997,17 @@ async function loadConfig(loadTools = true) {
}
} catch (error) {
console.error('加载配置失败:', error);
const baseMsg = (typeof window !== 'undefined' && typeof window.t === 'function')
? window.t('settings.apply.loadFailed')
: '加载配置失败';
alert(baseMsg + ': ' + error.message);
if (!silent) {
const baseMsg = (typeof window !== 'undefined' && typeof window.t === 'function')
? window.t('settings.apply.loadFailed')
: '加载配置失败';
if (typeof notifyApiError === 'function') {
notifyApiError(baseMsg + ': ' + error.message);
} else {
alert(baseMsg + ': ' + error.message);
}
}
throw error;
}
}
@@ -1050,6 +1061,9 @@ async function loadToolsList(page = 1, searchKeyword = '', options = {}) {
clearTimeout(timeoutId);
if (!response.ok) {
if (typeof readApiError === 'function') {
throw new Error(await readApiError(response, '获取工具列表失败'));
}
throw new Error('获取工具列表失败');
}
@@ -2790,6 +2804,7 @@ async function testOpenAIConnection() {
// 保存工具配置(独立函数,用于MCP管理页面)
async function saveToolsConfig() {
if (typeof requirePermission === 'function' && !requirePermission('config:write')) return;
try {
// 先保存当前页的状态到全局映射
saveCurrentPageToolStates();
@@ -3004,7 +3019,12 @@ let currentEditingMCPName = null;
// 拉取外部MCP列表数据(供轮询使用,返回 { servers, stats }
async function fetchExternalMCPs() {
const response = await apiFetch('/api/external-mcp');
if (!response.ok) throw new Error('获取外部MCP列表失败');
if (!response.ok) {
if (typeof readApiError === 'function') {
throw new Error(await readApiError(response, '获取外部MCP列表失败'));
}
throw new Error('获取外部MCP列表失败');
}
return response.json();
}
@@ -3207,6 +3227,7 @@ function renderExternalMCPStats(stats) {
// 显示添加外部MCP模态框
function showAddExternalMCPModal() {
if (typeof requirePermission === 'function' && !requirePermission('mcp:write')) return;
currentEditingMCPName = null;
document.getElementById('external-mcp-modal-title').textContent = (typeof window.t === 'function' ? window.t('mcp.addExternalMCP') : '添加外部MCP');
document.getElementById('external-mcp-json').value = '';
@@ -3316,6 +3337,7 @@ function loadExternalMCPExample() {
// 保存外部MCP
async function saveExternalMCP() {
if (typeof requirePermission === 'function' && !requirePermission('mcp:write')) return;
const jsonTextarea = document.getElementById('external-mcp-json');
const jsonStr = jsonTextarea.value.trim();
const errorDiv = document.getElementById('external-mcp-json-error');
+2
View File
@@ -472,6 +472,7 @@ function wireSkillModalOnce() {
}
function showAddSkillModal() {
if (typeof requirePermission === 'function' && !requirePermission('skills:write')) return;
wireSkillModalOnce();
const modal = document.getElementById('skill-modal');
if (!modal) return;
@@ -739,6 +740,7 @@ function closeSkillModal() {
// 保存skill
async function saveSkill() {
if (typeof requirePermission === 'function' && !requirePermission('skills:write')) return;
if (isSavingSkill) return;
const name = document.getElementById('skill-name').value.trim();
+2
View File
@@ -988,6 +988,7 @@ document.addEventListener('DOMContentLoaded', function() {
// 创建批量任务队列
async function createBatchQueue() {
if (typeof requirePermission === 'function' && !requirePermission('tasks:write')) return;
const input = document.getElementById('batch-tasks-input');
const titleInput = document.getElementById('batch-queue-title');
const roleSelect = document.getElementById('batch-queue-role');
@@ -2250,6 +2251,7 @@ async function saveInlineTask(queueId, taskId) {
// 显示添加批量任务模态框
function showAddBatchTaskModal() {
if (typeof requirePermission === 'function' && !requirePermission('tasks:write')) return;
const queueId = batchQueuesState.currentQueueId;
if (!queueId) {
alert(_t('tasks.queueInfoMissing'));
+4 -1
View File
@@ -1422,7 +1422,7 @@ function renderVulnerabilities(vulnerabilities, renderOptions) {
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button class="btn-ghost" onclick="deleteVulnerability('${vuln.id}')" title="${deleteTitle}">
<button class="btn-ghost" data-require-permission="vulnerability:delete" onclick="deleteVulnerability('${vuln.id}')" title="${deleteTitle}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1462,6 +1462,7 @@ function renderVulnerabilities(vulnerabilities, renderOptions) {
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(listContainer);
}
if (typeof rbacAfterDynamicRender === 'function') rbacAfterDynamicRender(listContainer);
restoreExpandedVulnerabilityDetails(opts.expandedIds);
@@ -1596,6 +1597,7 @@ async function populateVulnerabilityModalProjectSelect(selectedId) {
// 显示添加漏洞模态框
async function showAddVulnerabilityModal() {
if (typeof requirePermission === 'function' && !requirePermission('vulnerability:write')) return;
currentVulnerabilityId = null;
document.getElementById('vulnerability-modal-title').textContent = vulnT('vulnerability.addVuln');
@@ -1659,6 +1661,7 @@ async function editVulnerability(id) {
// 保存漏洞
async function saveVulnerability() {
if (typeof requirePermission === 'function' && !requirePermission('vulnerability:write')) return;
const conversationId = document.getElementById('vulnerability-conversation-id').value.trim();
const title = document.getElementById('vulnerability-title').value.trim();
const description = document.getElementById('vulnerability-description').value.trim();
+9 -4
View File
@@ -2240,10 +2240,13 @@ function selectWebshell(id, stateReady) {
'</div>' +
'<div id="ws-project-list" class="role-selection-list-main"></div>' +
'<div class="chat-project-panel-footer">' +
'<button type="button" class="role-selection-item-main chat-project-panel-create-btn" onclick="showNewProjectModalFromWebshellAi()">' +
'<span class="chat-project-panel-create-icon" aria-hidden="true">+</span>' +
'<span class="chat-project-panel-create-label">' + escapeHtml(wsProjectT('projects.newProject', '新建项目')) + '</span>' +
'</button></div></div></div></div>' +
((typeof hasPermission === 'function' && hasPermission('project:write'))
? ('<button type="button" class="role-selection-item-main chat-project-panel-create-btn" onclick="showNewProjectModalFromWebshellAi()">' +
'<span class="chat-project-panel-create-icon" aria-hidden="true">+</span>' +
'<span class="chat-project-panel-create-label">' + escapeHtml(wsProjectT('projects.newProject', '新建项目')) + '</span>' +
'</button>')
: '') +
'</div></div></div></div>' +
'<div class="ws-role-selector-wrapper">' +
'<button type="button" class="role-selector-btn ws-role-selector-btn" id="ws-role-selector-btn" onclick="wsToggleRolePanel()">' +
'<span id="ws-role-selector-icon" class="role-selector-icon">\ud83d\udd35</span>' +
@@ -4604,6 +4607,7 @@ function deleteWebshell(id) {
// 打开添加连接弹窗
function showAddWebshellModal() {
if (typeof requirePermission === 'function' && !requirePermission('webshell:write')) return;
var editIdEl = document.getElementById('webshell-edit-id');
if (editIdEl) editIdEl.value = '';
document.getElementById('webshell-url').value = '';
@@ -4920,6 +4924,7 @@ function testWebshellConnection() {
// 保存连接(新建或更新,请求服务端写入 SQLite 后刷新列表)
function saveWebshellConnection() {
if (typeof requirePermission === 'function' && !requirePermission('webshell:write')) return;
var url = (document.getElementById('webshell-url') || {}).value;
if (url && typeof url.trim === 'function') url = url.trim();
if (!url) {
+1
View File
@@ -1613,6 +1613,7 @@
window.closeWorkflowDryRunPanel = closeWorkflowDryRunPanel;
window.saveWorkflowDraft = async function () {
if (typeof requirePermission === 'function' && !requirePermission('workflow:write')) return;
initCy();
const meta = readWorkflowMetaFromForm();
if (!meta.id || !meta.name) {
+115 -114
View File
@@ -118,7 +118,7 @@
<div id="notification-dropdown" class="notification-dropdown" style="display: none;">
<div class="notification-dropdown-header">
<span id="notification-dropdown-title" data-i18n="notifications.title">事件通知</span>
<button class="notification-mark-read-btn" id="notification-mark-all-read-btn" type="button" onclick="markAllNotificationsSeen()" data-i18n="notifications.markAllRead">标记已读</button>
<button class="notification-mark-read-btn" id="notification-mark-all-read-btn" type="button" data-require-permission="notification:write" onclick="markAllNotificationsSeen()" data-i18n="notifications.markAllRead">标记已读</button>
</div>
<div id="notification-list" class="notification-list">
<div class="notification-empty" data-i18n="notifications.empty">暂无新事件</div>
@@ -867,7 +867,7 @@
<aside class="conversation-sidebar" id="conversation-sidebar">
<!-- 头部一行:折叠与「新对话」并排,避免绝对定位重叠(flex 为最佳实践) -->
<div class="sidebar-header conversation-sidebar-header">
<button type="button" class="new-chat-btn" onclick="startNewConversation()">
<button type="button" class="new-chat-btn" data-require-permission="chat:write" onclick="startNewConversation()">
<span>+</span> <span data-i18n="chat.newChat">新对话</span>
</button>
<button type="button" class="conversation-sidebar-collapse-btn" onclick="toggleConversationSidebar()" data-i18n="chat.toggleConversationPanel" data-i18n-attr="title" data-i18n-skip-text="true" title="折叠/展开对话列表" aria-label="折叠/展开对话列表">
@@ -904,7 +904,7 @@
<div class="conversation-groups-section">
<div class="section-header">
<span class="section-title" data-i18n="chat.conversationGroups">对话分组</span>
<button class="add-group-btn" onclick="showCreateGroupModal()" data-i18n="chat.addGroup" data-i18n-attr="title" data-i18n-skip-text="true" title="新建分组">
<button class="add-group-btn" data-require-permission="group:write" onclick="showCreateGroupModal()" data-i18n="chat.addGroup" data-i18n-attr="title" data-i18n-skip-text="true" title="新建分组">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -949,7 +949,7 @@
</button>
</div>
</div>
<button class="batch-manage-btn" onclick="showBatchManageModal()" data-i18n="chat.batchManage" data-i18n-attr="title" data-i18n-skip-text="true" title="批量管理">
<button class="batch-manage-btn" data-require-permission="chat:delete" onclick="showBatchManageModal()" data-i18n="chat.batchManage" data-i18n-attr="title" data-i18n-skip-text="true" title="批量管理">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="3" y1="12" x2="21" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="3" y1="6" x2="21" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
@@ -1061,7 +1061,7 @@
<p class="hitl-config-hint" data-i18n="chat.hitlWhitelistHint">每行一个或逗号分隔;与 config 中全局白名单合并展示。</p>
</div>
<div class="hitl-config-actions">
<button type="button" class="hitl-apply-btn" id="hitl-apply-btn" onclick="window.applyHitlSidebarConfig && window.applyHitlSidebarConfig()">
<button type="button" class="hitl-apply-btn" data-require-permission="hitl:write" id="hitl-apply-btn" onclick="window.applyHitlSidebarConfig && window.applyHitlSidebarConfig()">
<span data-i18n="chat.hitlApply">应用</span>
</button>
</div>
@@ -1086,13 +1086,13 @@
<path d="m21 21-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<button class="group-action-btn" onclick="editGroup()" data-i18n="chatGroup.edit" data-i18n-attr="title" title="编辑">
<button class="group-action-btn" data-require-permission="group:write" onclick="editGroup()" data-i18n="chatGroup.edit" data-i18n-attr="title" title="编辑">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button class="group-action-btn delete-btn" onclick="deleteGroup()" data-i18n="chatGroup.delete" data-i18n-attr="title" title="删除">
<button class="group-action-btn delete-btn" data-require-permission="group:delete" onclick="deleteGroup()" data-i18n="chatGroup.delete" data-i18n-attr="title" title="删除">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1157,7 +1157,7 @@
</div>
<div id="chat-project-list" class="role-selection-list-main"></div>
<div class="chat-project-panel-footer">
<button type="button" class="role-selection-item-main chat-project-panel-create-btn" onclick="showNewProjectModalFromChat()">
<button type="button" class="role-selection-item-main chat-project-panel-create-btn" data-require-permission="project:write" onclick="showNewProjectModalFromChat()">
<span class="chat-project-panel-create-icon" aria-hidden="true">+</span>
<span class="chat-project-panel-create-label" data-i18n="projects.newProject">新建项目</span>
</button>
@@ -1260,7 +1260,7 @@
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button type="button" class="send-btn" id="chat-send-btn" onclick="sendMessage()">
<button type="button" class="send-btn" id="chat-send-btn" data-require-permission="chat:write" onclick="sendMessage()">
<span data-i18n="chat.send">发送</span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
@@ -1347,7 +1347,7 @@
</select>
</label>
<button type="button" class="btn-secondary" onclick="filterHitlLogs()" data-i18n="hitl.searchApply">搜索</button>
<button type="button" class="btn-secondary btn-delete" onclick="clearHitlLogs()" data-i18n="hitl.clearAll">清空</button>
<button type="button" class="btn-secondary btn-delete" data-require-permission="hitl:write" onclick="clearHitlLogs()" data-i18n="hitl.clearAll">清空</button>
</div>
<p id="hitl-logs-retention-hint" class="hitl-logs-retention-hint" hidden></p>
<div id="hitl-logs-batch-actions" class="monitor-batch-actions" style="display: none;">
@@ -1357,7 +1357,7 @@
<div class="batch-actions-buttons">
<button type="button" class="btn-secondary" onclick="selectAllHitlLogs()" data-i18n="hitl.selectAll">全选</button>
<button type="button" class="btn-secondary" onclick="deselectAllHitlLogs()" data-i18n="hitl.deselectAll">取消全选</button>
<button type="button" class="btn-secondary btn-delete" onclick="batchDeleteHitlLogs()" data-i18n="hitl.batchDelete">批量删除</button>
<button type="button" class="btn-secondary btn-delete" data-require-permission="hitl:write" onclick="batchDeleteHitlLogs()" data-i18n="hitl.batchDelete">批量删除</button>
</div>
</div>
<div id="hitl-logs-table-wrap" class="hitl-logs-table-wrap">
@@ -1372,7 +1372,7 @@
<span class="hitl-page-strategy-label" data-i18n="hitl.strategyLabel">审计策略</span>
<div class="hitl-page-strategy-actions">
<button type="button" class="btn-link" id="hitl-strategy-reset-btn" onclick="resetHitlAuditStrategy()" data-i18n="hitl.strategyReset">恢复默认</button>
<button type="button" class="btn-secondary" id="hitl-strategy-save-btn" onclick="saveHitlAuditStrategy()" data-i18n="common.save">保存</button>
<button type="button" class="btn-secondary" data-require-permission="hitl:write" id="hitl-strategy-save-btn" onclick="saveHitlAuditStrategy()" data-i18n="common.save">保存</button>
</div>
</div>
<div class="hitl-strategy-subtabs" role="tablist" aria-label="Audit strategy mode">
@@ -1391,7 +1391,7 @@
<div class="hitl-page-whitelist-bar" id="hitl-page-whitelist-bar">
<div class="hitl-page-whitelist-header">
<span class="hitl-page-whitelist-label" data-i18n="hitl.whitelistLabel">免审批工具白名单</span>
<button type="button" class="btn-secondary" id="hitl-page-whitelist-save-btn" onclick="saveHitlPageWhitelist()" data-i18n="common.save">保存</button>
<button type="button" class="btn-secondary" data-require-permission="hitl:write" id="hitl-page-whitelist-save-btn" onclick="saveHitlPageWhitelist()" data-i18n="common.save">保存</button>
</div>
<p class="hitl-page-whitelist-hint" data-i18n="hitl.whitelistHint">每行一个或逗号分隔;保存后写入 config.yaml 全局白名单并立即生效(与聊天侧栏同步展示)。</p>
<textarea id="hitl-page-sensitive-tools" class="hitl-page-whitelist-textarea" rows="6" spellcheck="false" autocomplete="off" data-i18n="chat.hitlWhitelistPlaceholder" data-i18n-attr="placeholder" placeholder=""></textarea>
@@ -1499,7 +1499,7 @@
<div class="batch-actions-buttons">
<button class="btn-secondary" onclick="selectAllExecutions()" data-i18n="mcp.selectAll">全选</button>
<button class="btn-secondary" onclick="deselectAllExecutions()" data-i18n="mcpMonitor.deselectAll">取消全选</button>
<button class="btn-secondary btn-delete" onclick="batchDeleteExecutions()" data-i18n="mcp.deleteSelected">批量删除</button>
<button class="btn-secondary btn-delete" data-require-permission="monitor:delete" onclick="batchDeleteExecutions()" data-i18n="mcp.deleteSelected">批量删除</button>
</div>
</div>
<div id="monitor-executions" class="monitor-table-container">
@@ -1524,7 +1524,7 @@
<div class="settings-section mcp-management-panel mcp-tools-panel" style="margin-bottom: 32px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h3 style="margin: 0;" data-i18n="mcp.toolConfig">MCP 工具配置</h3>
<button class="btn-primary" onclick="saveToolsConfig()" data-i18n="mcp.saveToolConfig">保存工具配置</button>
<button class="btn-primary" data-require-permission="config:write" onclick="saveToolsConfig()" data-i18n="mcp.saveToolConfig">保存工具配置</button>
</div>
<div class="tools-controls mcp-panel-body">
<div class="tools-actions">
@@ -1549,7 +1549,7 @@
<div class="settings-section mcp-management-panel mcp-external-panel">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h3 style="margin: 0;" data-i18n="mcp.externalConfig">外部 MCP 配置</h3>
<button class="btn-primary" onclick="showAddExternalMCPModal()" data-i18n="mcp.addExternalMCP">添加外部MCP</button>
<button class="btn-primary" data-require-permission="mcp:write" onclick="showAddExternalMCPModal()" data-i18n="mcp.addExternalMCP">添加外部MCP</button>
</div>
<div class="external-mcp-controls mcp-panel-body">
<div class="external-mcp-actions">
@@ -1568,9 +1568,9 @@
<h2 data-i18n="knowledge.title">知识管理</h2>
<div class="page-header-actions">
<button class="btn-secondary" onclick="refreshKnowledgeBase()" data-i18n="common.refresh">刷新</button>
<button class="btn-secondary" onclick="buildKnowledgeIndex()" data-i18n="knowledge.buildIndex">构建索引</button>
<button class="btn-secondary" onclick="rebuildKnowledgeIndexFull()" data-i18n="knowledge.rebuildIndexFull">全量重建</button>
<button class="btn-primary" onclick="showAddKnowledgeItemModal()" data-i18n="knowledge.addKnowledge">添加知识</button>
<button class="btn-secondary" data-require-permission="knowledge:write" onclick="buildKnowledgeIndex()" data-i18n="knowledge.buildIndex">构建索引</button>
<button class="btn-secondary" data-require-permission="knowledge:write" onclick="rebuildKnowledgeIndexFull()" data-i18n="knowledge.rebuildIndexFull">全量重建</button>
<button class="btn-primary" data-require-permission="knowledge:write" onclick="showAddKnowledgeItemModal()" data-i18n="knowledge.addKnowledge">添加知识</button>
</div>
</div>
<div class="page-content">
@@ -1673,7 +1673,7 @@
<h2 data-i18n="infoCollectPage.title">信息收集</h2>
<div class="page-header-actions">
<button class="btn-secondary" onclick="resetFofaForm()" data-i18n="infoCollectPage.reset">重置</button>
<button class="btn-primary" onclick="submitFofaSearch()" data-i18n="infoCollectPage.confirm">确定</button>
<button class="btn-primary" data-require-permission="fofa:execute" onclick="submitFofaSearch()" data-i18n="infoCollectPage.confirm">确定</button>
</div>
</div>
<div class="page-content">
@@ -1737,10 +1737,10 @@
<div class="info-collect-results-toolbar" aria-label="结果工具条" data-i18n="infoCollectPage.resultsToolbarAria" data-i18n-attr="aria-label" data-i18n-skip-text="true">
<div class="info-collect-selected" id="fofa-selected-meta" data-i18n="infoCollectPage.selectedRowsZero">已选择 0 条</div>
<button class="btn-secondary btn-small" type="button" onclick="toggleFofaColumnsPanel()" data-i18n="infoCollectPage.showHideColumns" data-i18n-attr="title" title="显示/隐藏字段"><span data-i18n="infoCollectPage.columns"></span></button>
<button class="btn-secondary btn-small" type="button" onclick="exportFofaResults('csv')" data-i18n="infoCollectPage.exportCsvTitle" data-i18n-attr="title" title="导出当前结果为 CSVUTF-8,兼容中文)"><span data-i18n="infoCollectPage.exportCsv">导出 CSV</span></button>
<button class="btn-secondary btn-small" type="button" onclick="exportFofaResults('json')" data-i18n="infoCollectPage.exportJsonTitle" data-i18n-attr="title" title="导出当前结果为 JSON"><span data-i18n="infoCollectPage.exportJson">导出 JSON</span></button>
<button class="btn-secondary btn-small" type="button" onclick="exportFofaResults('xlsx')" data-i18n="infoCollectPage.exportXlsxTitle" data-i18n-attr="title" title="导出当前结果为 Excel"><span data-i18n="infoCollectPage.exportXlsx">导出 XLSX</span></button>
<button class="btn-primary btn-small" type="button" onclick="batchScanSelectedFofaRows()" data-i18n="infoCollectPage.batchScanTitle" data-i18n-attr="title" title="将所选行创建为批量任务队列"><span data-i18n="infoCollectPage.batchScan">批量扫描</span></button>
<button class="btn-secondary btn-small" type="button" data-require-permission="fofa:execute" onclick="exportFofaResults('csv')" data-i18n="infoCollectPage.exportCsvTitle" data-i18n-attr="title" title="导出当前结果为 CSVUTF-8,兼容中文)"><span data-i18n="infoCollectPage.exportCsv">导出 CSV</span></button>
<button class="btn-secondary btn-small" type="button" data-require-permission="fofa:execute" onclick="exportFofaResults('json')" data-i18n="infoCollectPage.exportJsonTitle" data-i18n-attr="title" title="导出当前结果为 JSON"><span data-i18n="infoCollectPage.exportJson">导出 JSON</span></button>
<button class="btn-secondary btn-small" type="button" data-require-permission="fofa:execute" onclick="exportFofaResults('xlsx')" data-i18n="infoCollectPage.exportXlsxTitle" data-i18n-attr="title" title="导出当前结果为 Excel"><span data-i18n="infoCollectPage.exportXlsx">导出 XLSX</span></button>
<button class="btn-primary btn-small" data-require-permission="fofa:execute" type="button" onclick="batchScanSelectedFofaRows()" data-i18n="infoCollectPage.batchScanTitle" data-i18n-attr="title" title="将所选行创建为批量任务队列"><span data-i18n="infoCollectPage.batchScan">批量扫描</span></button>
</div>
</div>
<div class="info-collect-results-table-wrap">
@@ -1774,7 +1774,7 @@
<div class="page-header-actions">
<label class="projects-show-archived-label"><input type="checkbox" id="projects-show-archived" onchange="loadProjectsList()"> <span data-i18n="projects.showArchived">显示已归档</span></label>
<button class="btn-secondary" type="button" onclick="loadProjectsList()" data-i18n="common.refresh">刷新</button>
<button class="btn-primary" type="button" onclick="showNewProjectModal()" data-i18n="projects.newProjectCta">+ 新建项目</button>
<button class="btn-primary" type="button" data-require-permission="project:write" onclick="showNewProjectModal()" data-i18n="projects.newProjectCta">+ 新建项目</button>
</div>
</div>
<div class="page-content projects-page-layout">
@@ -1800,7 +1800,7 @@
</div>
<h3 data-i18n="projects.selectOrCreateTitle">选择或创建项目</h3>
<p data-i18n="projects.selectOrCreateHint">项目用于跨对话共享「事实黑板」:目标、环境、认证等信息会在绑定项目的对话中自动注入。</p>
<button class="btn-primary" type="button" onclick="showNewProjectModal()" data-i18n="projects.createFirstProject">创建第一个项目</button>
<button class="btn-primary" type="button" data-require-permission="project:write" onclick="showNewProjectModal()" data-i18n="projects.createFirstProject">创建第一个项目</button>
</div>
<div class="projects-detail-inner" id="projects-detail-inner" hidden>
<header class="projects-detail-header">
@@ -1825,7 +1825,7 @@
<span id="projects-detail-meta-time" class="projects-detail-meta-time"></span>
</span>
<button type="button" class="btn-secondary btn-small" onclick="openVulnerabilitiesForProject()" data-i18n="projects.vulnerabilityManagement">漏洞管理</button>
<button type="button" class="btn-primary btn-small" onclick="showAddFactModal()" data-i18n="projects.addFactCta">+ 添加事实</button>
<button type="button" class="btn-primary btn-small" data-require-permission="project:write" onclick="showAddFactModal()" data-i18n="projects.addFactCta">+ 添加事实</button>
</div>
</header>
<nav class="projects-tabs" role="tablist">
@@ -1930,7 +1930,7 @@
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span data-i18n="projects.graphCenter">居中</span>
</button>
<button type="button" class="projects-graph-action-btn projects-graph-action-btn--connect" id="project-graph-connect-btn" onclick="toggleProjectFactGraphConnectMode()" data-i18n="projects.graphConnect">连边</button>
<button type="button" class="projects-graph-action-btn projects-graph-action-btn--connect" data-require-permission="project:write" id="project-graph-connect-btn" onclick="toggleProjectFactGraphConnectMode()" data-i18n="projects.graphConnect">连边</button>
</div>
</div>
</div>
@@ -1954,7 +1954,7 @@
</div>
<div class="project-fact-graph-sidebar-actions">
<button type="button" class="btn-primary btn-small" id="project-fact-graph-detail-btn" onclick="openSelectedGraphFactDetail()" data-i18n="projects.details">详情</button>
<button type="button" class="btn-secondary btn-small" id="project-fact-graph-edit-btn" onclick="editSelectedGraphFact()" data-i18n="common.edit">编辑</button>
<button type="button" class="btn-secondary btn-small" data-require-permission="project:write" id="project-fact-graph-edit-btn" onclick="editSelectedGraphFact()" data-i18n="common.edit">编辑</button>
</div>
</aside>
</div>
@@ -2135,14 +2135,14 @@
</div>
</div>
<div class="projects-settings-danger-actions">
<button class="btn-secondary btn-small" type="button" onclick="archiveCurrentProject()" data-i18n="projects.archiveRestore">归档 / 恢复</button>
<button class="btn-secondary btn-small btn-danger-outline" type="button" onclick="deleteCurrentProject()" data-i18n="projects.deleteProject">删除项目</button>
<button class="btn-secondary btn-small" data-require-permission="project:write" type="button" onclick="archiveCurrentProject()" data-i18n="projects.archiveRestore">归档 / 恢复</button>
<button class="btn-secondary btn-small btn-danger-outline" data-require-permission="project:delete" type="button" onclick="deleteCurrentProject()" data-i18n="projects.deleteProject">删除项目</button>
</div>
</section>
</div>
<footer class="projects-settings-footer">
<span class="projects-settings-footer-hint" data-i18n="projects.saveChangesHint">修改后请点击保存以同步到服务器</span>
<button class="btn-primary" type="button" onclick="saveProjectSettings()">
<button class="btn-primary" data-require-permission="project:write" type="button" onclick="saveProjectSettings()">
<span data-i18n="projects.saveSettings">保存更改</span>
</button>
</footer>
@@ -2156,10 +2156,10 @@
<div class="page-header">
<h2 data-i18n="vulnerability.title">漏洞管理</h2>
<div class="page-header-actions">
<button class="btn-secondary" onclick="exportVulnerabilityReports()" data-i18n="vulnerabilityPage.batchExport">批量导出</button>
<button class="btn-secondary btn-delete" onclick="batchDeleteVulnerabilityReports()" data-i18n="vulnerabilityPage.batchDelete">批量删除</button>
<button class="btn-secondary" data-require-permission="vulnerability:read" onclick="exportVulnerabilityReports()" data-i18n="vulnerabilityPage.batchExport">批量导出</button>
<button class="btn-secondary btn-delete" data-require-permission="vulnerability:delete" onclick="batchDeleteVulnerabilityReports()" data-i18n="vulnerabilityPage.batchDelete">批量删除</button>
<button class="btn-secondary" onclick="refreshVulnerabilities()" data-i18n="common.refresh">刷新</button>
<button class="btn-primary" onclick="showAddVulnerabilityModal()" data-i18n="vulnerability.addVuln">添加漏洞</button>
<button class="btn-primary" data-require-permission="vulnerability:write" onclick="showAddVulnerabilityModal()" data-i18n="vulnerability.addVuln">添加漏洞</button>
</div>
</div>
<div class="page-content">
@@ -2322,7 +2322,7 @@
<div class="page-header">
<h2 data-i18n="webshell.title">WebShell 管理</h2>
<div class="page-header-actions">
<button class="btn-primary" onclick="showAddWebshellModal()" data-i18n="webshell.addConnection">添加连接</button>
<button class="btn-primary" data-require-permission="webshell:write" onclick="showAddWebshellModal()" data-i18n="webshell.addConnection">添加连接</button>
</div>
</div>
<div class="page-content webshell-page-content">
@@ -2359,7 +2359,7 @@
<div class="page-header">
<h2 data-i18n="chatFilesPage.title">文件管理</h2>
<div class="page-header-actions">
<button type="button" class="btn-primary" id="chat-files-header-upload-btn" onclick="chatFilesOpenUploadPicker()" data-i18n="chatFilesPage.upload">上传文件</button>
<button type="button" class="btn-primary" id="chat-files-header-upload-btn" data-require-permission="files:write" onclick="chatFilesOpenUploadPicker()" data-i18n="chatFilesPage.upload">上传文件</button>
<input type="file" id="chat-files-upload-input" style="display:none" multiple onchange="onChatFilesUploadPick(event)" />
<button type="button" class="btn-secondary" id="chat-files-refresh-btn" onclick="loadChatFilesPage()" data-i18n="common.refresh">刷新</button>
</div>
@@ -2401,7 +2401,7 @@
<div class="page-header">
<h2 data-i18n="tasks.title">任务管理</h2>
<div class="page-header-actions">
<button class="btn-primary" onclick="showBatchImportModal()" data-i18n="tasks.newTask">新建任务</button>
<button class="btn-primary" data-require-permission="tasks:write" onclick="showBatchImportModal()" data-i18n="tasks.newTask">新建任务</button>
<label class="auto-refresh-toggle">
<input type="checkbox" id="tasks-auto-refresh" checked onchange="toggleTasksAutoRefresh(this.checked)">
<span data-i18n="tasks.autoRefresh">自动刷新</span>
@@ -2444,7 +2444,7 @@
<div class="page-header">
<h2 data-i18n="c2.listeners.title">监听器管理</h2>
<div class="page-header-actions">
<button class="btn-primary" onclick="C2.showCreateListenerModal()" data-i18n="c2.listeners.headerCreateBtn">+ 创建监听器</button>
<button class="btn-primary" data-require-permission="c2:write" onclick="C2.showCreateListenerModal()" data-i18n="c2.listeners.headerCreateBtn">+ 创建监听器</button>
</div>
</div>
<div class="page-content">
@@ -2457,8 +2457,8 @@
<div class="page-header">
<h2 data-i18n="c2.sessions.title">会话管理</h2>
<div class="page-header-actions">
<button type="button" class="btn-danger" id="c2-sessions-batch-delete" disabled onclick="C2.deleteSelectedSessions()"><span data-i18n="c2.sessions.batchDelete">批量删除</span></button>
<button type="button" class="btn-secondary" id="c2-sessions-delete-filtered" disabled onclick="C2.deleteFilteredSessions()"><span data-i18n="c2.sessions.deleteFiltered">删除筛选结果</span></button>
<button type="button" class="btn-danger" id="c2-sessions-batch-delete" data-require-permission="c2:delete" disabled onclick="C2.deleteSelectedSessions()"><span data-i18n="c2.sessions.batchDelete">批量删除</span></button>
<button type="button" class="btn-secondary" id="c2-sessions-delete-filtered" data-require-permission="c2:delete" disabled onclick="C2.deleteFilteredSessions()"><span data-i18n="c2.sessions.deleteFiltered">删除筛选结果</span></button>
<button class="btn-secondary" onclick="C2.loadSessions()"><span data-i18n="common.refresh">刷新</span></button>
</div>
</div>
@@ -2478,7 +2478,7 @@
<div class="page-header">
<h2 data-i18n="c2.tasks.title">任务管理</h2>
<div class="page-header-actions">
<button type="button" class="btn-danger" id="c2-tasks-batch-delete" disabled onclick="C2.deleteSelectedTasks()"><span data-i18n="c2.tasks.batchDelete">批量删除</span></button>
<button type="button" class="btn-danger" id="c2-tasks-batch-delete" data-require-permission="c2:delete" disabled onclick="C2.deleteSelectedTasks()"><span data-i18n="c2.tasks.batchDelete">批量删除</span></button>
<button type="button" class="btn-secondary" onclick="C2.loadTasks()"><span data-i18n="common.refresh">刷新</span></button>
</div>
</div>
@@ -2521,7 +2521,7 @@
<label data-i18n="c2.payloads.hostOptional">回连地址 (可选)</label>
<input type="text" id="c2-payload-host" class="form-control" data-i18n="c2.payloads.placeholderListenerHost" data-i18n-attr="placeholder" placeholder="留空则使用监听器地址">
</div>
<button type="button" id="c2-generate-oneliner-btn" class="btn-primary" onclick="C2.generateOneliner()" style="width:100%;" data-i18n="c2.payloads.generateOnelinerBtn">生成 Oneliner</button>
<button type="button" id="c2-generate-oneliner-btn" class="btn-primary" data-require-permission="c2:write" onclick="C2.generateOneliner()" style="width:100%;" data-i18n="c2.payloads.generateOnelinerBtn">生成 Oneliner</button>
<pre id="c2-oneliner-output" class="c2-oneliner-output" style="display:none;" onclick="C2.copyOneliner()" data-i18n="c2.payloads.clickToCopyTitle" data-i18n-attr="title" data-i18n-skip-text="true" title="点击复制"></pre>
</div>
<div class="c2-payload-card">
@@ -2559,7 +2559,7 @@
</select>
</div>
</div>
<button id="c2-build-btn" type="button" class="btn-primary" onclick="C2.buildBeacon()" style="width:100%;" data-i18n="c2.payloads.buildBeaconBtn">构建 Beacon</button>
<button id="c2-build-btn" type="button" class="btn-primary" data-require-permission="c2:write" onclick="C2.buildBeacon()" style="width:100%;" data-i18n="c2.payloads.buildBeaconBtn">构建 Beacon</button>
<div id="c2-build-result"></div>
</div>
</div>
@@ -2571,7 +2571,7 @@
<div class="page-header">
<h2 data-i18n="c2.events.title">事件审计</h2>
<div class="page-header-actions">
<button type="button" class="btn-danger" id="c2-events-batch-delete" disabled onclick="C2.deleteSelectedEvents()"><span data-i18n="c2.events.batchDelete">批量删除</span></button>
<button type="button" class="btn-danger" id="c2-events-batch-delete" data-require-permission="c2:delete" disabled onclick="C2.deleteSelectedEvents()"><span data-i18n="c2.events.batchDelete">批量删除</span></button>
<button type="button" class="btn-secondary" onclick="C2.loadEvents()"><span data-i18n="common.refresh">刷新</span></button>
</div>
</div>
@@ -2592,7 +2592,7 @@
<div class="page-header">
<h2 data-i18n="c2.profiles.title">流量伪装</h2>
<div class="page-header-actions">
<button class="btn-primary" onclick="C2.showCreateProfileModal()" data-i18n="c2.profiles.createBtn">+ 创建 Profile</button>
<button class="btn-primary" data-require-permission="c2:write" onclick="C2.showCreateProfileModal()" data-i18n="c2.profiles.createBtn">+ 创建 Profile</button>
</div>
</div>
<div class="page-content">
@@ -2631,13 +2631,13 @@
<h3 data-i18n="workflows.nodeLibrary">节点库</h3>
</div>
<div class="workflow-node-palette">
<button type="button" draggable="true" data-node-type="start" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('start')" data-i18n="workflows.nodes.start">开始</button>
<button type="button" draggable="true" data-node-type="tool" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('tool')" data-i18n="workflows.nodes.tool">工具</button>
<button type="button" draggable="true" data-node-type="agent" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('agent')" data-i18n="workflows.nodes.agent">Agent</button>
<button type="button" draggable="true" data-node-type="condition" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('condition')" data-i18n="workflows.nodes.condition">条件</button>
<button type="button" draggable="true" data-node-type="hitl" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('hitl')" data-i18n="workflows.nodes.hitl">审批</button>
<button type="button" draggable="true" data-node-type="output" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('output')" data-i18n="workflows.nodes.output">输出</button>
<button type="button" draggable="true" data-node-type="end" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('end')" data-i18n="workflows.nodes.end">结束</button>
<button type="button" draggable="true" data-require-permission="workflow:write" data-node-type="start" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('start')" data-i18n="workflows.nodes.start">开始</button>
<button type="button" draggable="true" data-require-permission="workflow:write" data-node-type="tool" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('tool')" data-i18n="workflows.nodes.tool">工具</button>
<button type="button" draggable="true" data-require-permission="workflow:write" data-node-type="agent" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('agent')" data-i18n="workflows.nodes.agent">Agent</button>
<button type="button" draggable="true" data-require-permission="workflow:write" data-node-type="condition" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('condition')" data-i18n="workflows.nodes.condition">条件</button>
<button type="button" draggable="true" data-require-permission="workflow:write" data-node-type="hitl" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('hitl')" data-i18n="workflows.nodes.hitl">审批</button>
<button type="button" draggable="true" data-require-permission="workflow:write" data-node-type="output" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('output')" data-i18n="workflows.nodes.output">输出</button>
<button type="button" draggable="true" data-require-permission="workflow:write" data-node-type="end" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('end')" data-i18n="workflows.nodes.end">结束</button>
</div>
</div>
</aside>
@@ -2650,12 +2650,12 @@
</div>
</div>
<div class="workflow-toolbar">
<button class="btn-secondary btn-small" type="button" onclick="toggleWorkflowConnectMode()" id="workflow-connect-btn" data-i18n="workflows.connect">连线</button>
<button class="btn-secondary btn-small" type="button" onclick="deleteWorkflowSelection()" data-i18n="workflows.deleteSelected">删除选中</button>
<button class="btn-secondary btn-small" data-require-permission="workflow:write" type="button" onclick="toggleWorkflowConnectMode()" id="workflow-connect-btn" data-i18n="workflows.connect">连线</button>
<button class="btn-secondary btn-small" data-require-permission="workflow:delete" type="button" onclick="deleteWorkflowSelection()" data-i18n="workflows.deleteSelected">删除选中</button>
<button class="btn-secondary btn-small" type="button" onclick="layoutWorkflowGraph()" data-i18n="workflows.autoLayout">自动布局</button>
<button class="btn-secondary btn-small" type="button" onclick="dryRunWorkflowDraft()" data-i18n="workflows.dryRun">试运行</button>
<button class="btn-secondary btn-small" onclick="deleteCurrentWorkflow()" data-i18n="common.delete">删除</button>
<button class="btn-primary btn-small" onclick="saveWorkflowDraft()" data-i18n="common.save">保存</button>
<button class="btn-secondary btn-small" data-require-permission="workflow:execute" type="button" onclick="dryRunWorkflowDraft()" data-i18n="workflows.dryRun">试运行</button>
<button class="btn-secondary btn-small" data-require-permission="workflow:delete" onclick="deleteCurrentWorkflow()" data-i18n="common.delete">删除</button>
<button class="btn-primary btn-small" data-require-permission="workflow:write" onclick="saveWorkflowDraft()" data-i18n="common.save">保存</button>
</div>
</section>
<section class="workflow-canvas-wrap" ondragover="workflowCanvasDragOver(event)" ondrop="workflowCanvasDrop(event)">
@@ -2666,7 +2666,7 @@
<aside class="workflow-properties">
<div class="workflow-panel-header">
<h3 id="workflow-property-title" data-i18n="workflows.properties">属性</h3>
<button type="button" id="workflow-property-delete-btn" class="btn-secondary btn-small" onclick="deleteWorkflowSelection()" hidden data-i18n="common.delete">删除</button>
<button type="button" id="workflow-property-delete-btn" class="btn-secondary btn-small" data-require-permission="workflow:delete" onclick="deleteWorkflowSelection()" hidden data-i18n="common.delete">删除</button>
</div>
<div id="workflow-property-empty" class="workflow-property-empty" data-i18n="workflows.propertyEmpty">选择一个节点或连线后编辑属性</div>
<div id="workflow-property-form" class="workflow-property-form" hidden>
@@ -2689,7 +2689,7 @@
<div id="workflow-typed-config" class="workflow-typed-config"></div>
<div class="workflow-custom-fields-head">
<span data-i18n="workflows.customFields">自定义字段</span>
<button type="button" class="btn-secondary btn-small" onclick="addWorkflowCustomField()" data-i18n="workflows.addField">添加字段</button>
<button type="button" class="btn-secondary btn-small" data-require-permission="workflow:write" onclick="addWorkflowCustomField()" data-i18n="workflows.addField">添加字段</button>
</div>
<div id="workflow-custom-fields" class="workflow-custom-fields"></div>
</div>
@@ -2714,7 +2714,7 @@
<h2 data-i18n="roles.title">角色管理</h2>
<div class="page-header-actions">
<button class="btn-secondary" onclick="refreshRoles()" data-i18n="common.refresh">刷新</button>
<button class="btn-primary" onclick="showAddRoleModal()" data-i18n="roles.createRole">创建角色</button>
<button class="btn-primary" data-require-permission="roles:write" onclick="showAddRoleModal()" data-i18n="roles.createRole">创建角色</button>
</div>
</div>
<div class="page-content">
@@ -2774,7 +2774,7 @@
<h2 data-i18n="skills.title">Skills管理</h2>
<div class="page-header-actions">
<button class="btn-secondary" onclick="refreshSkills()" data-i18n="common.refresh">刷新</button>
<button class="btn-primary" onclick="showAddSkillModal()" data-i18n="skills.createSkill">创建Skill</button>
<button class="btn-primary" data-require-permission="skills:write" onclick="showAddSkillModal()" data-i18n="skills.createSkill">创建Skill</button>
</div>
</div>
<div class="page-content page-content-with-pagination">
@@ -2820,7 +2820,7 @@
<h2 data-i18n="agentsPage.title">Agent 管理</h2>
<div class="page-header-actions">
<button type="button" class="btn-secondary" onclick="loadMarkdownAgents()" data-i18n="common.refresh">刷新</button>
<button type="button" class="btn-primary" onclick="showAddMarkdownAgentModal()" data-i18n="agentsPage.create">新建 Agent</button>
<button type="button" class="btn-primary" data-require-permission="agents:write" onclick="showAddMarkdownAgentModal()" data-i18n="agentsPage.create">新建 Agent</button>
</div>
</div>
<div class="page-content">
@@ -2884,7 +2884,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn-secondary" onclick="closeMarkdownAgentModal()" data-i18n="common.cancel">取消</button>
<button type="button" class="btn-primary" onclick="saveMarkdownAgent()" data-i18n="common.save">保存</button>
<button type="button" class="btn-primary" data-require-permission="agents:write" onclick="saveMarkdownAgent()" data-i18n="common.save">保存</button>
</div>
</div>
</div>
@@ -3164,7 +3164,7 @@
</div>
<div class="settings-actions">
<button class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
<button class="btn-primary" onclick="applySettings()" data-require-permission="config:write" data-i18n="settings.apply.button">应用配置</button>
</div>
</div>
@@ -3251,7 +3251,7 @@
</div>
<div class="settings-actions">
<button type="button" class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
<button type="button" class="btn-primary" onclick="applySettings()" data-require-permission="config:write" data-i18n="settings.apply.button">应用配置</button>
</div>
</div>
@@ -3283,7 +3283,7 @@
</div>
<div class="settings-actions">
<button type="button" class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
<button type="button" class="btn-primary" onclick="applySettings()" data-require-permission="config:write" data-i18n="settings.apply.button">应用配置</button>
</div>
</div>
@@ -3484,7 +3484,7 @@
</div>
<div class="settings-actions">
<button class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
<button class="btn-primary" onclick="applySettings()" data-require-permission="config:write" data-i18n="settings.apply.button">应用配置</button>
</div>
</div>
@@ -3507,7 +3507,7 @@
</div>
</div>
<div class="settings-actions">
<button class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
<button class="btn-primary" onclick="applySettings()" data-require-permission="config:write" data-i18n="settings.apply.button">应用配置</button>
</div>
</div>
@@ -3524,11 +3524,11 @@
<h4 data-i18n="settings.robots.managerTitle">机器人管理</h4>
<p data-i18n="settings.robots.managerDesc">先选择机器人类型,再进入对应配置;已配置的平台会显示连接状态。</p>
</div>
<button type="button" class="btn-primary" onclick="openRobotCreateModal()" data-i18n="settings.robots.newBot">新建机器人</button>
<button type="button" class="btn-primary" data-require-permission="robot:write" onclick="openRobotCreateModal()" data-i18n="settings.robots.newBot">新建机器人</button>
</div>
<div class="robot-card-grid">
<button type="button" class="robot-card" data-robot-card="wechat" onclick="openRobotEditor('wechat')">
<button type="button" class="robot-card" data-robot-card="wechat" data-require-permission="robot:write" onclick="openRobotEditor('wechat')">
<span class="robot-card-main">
<span class="robot-card-title" data-i18n="settings.robots.wechat.title">微信 / iLink</span>
<span class="robot-card-desc" data-i18n="settings.robots.wechat.subtitle">扫码绑定个人微信,在手机端直接与 CyberStrikeAI 对话</span>
@@ -3538,7 +3538,7 @@
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
</span>
</button>
<button type="button" class="robot-card" data-robot-card="wecom" onclick="openRobotEditor('wecom')">
<button type="button" class="robot-card" data-robot-card="wecom" data-require-permission="robot:write" onclick="openRobotEditor('wecom')">
<span class="robot-card-main">
<span class="robot-card-title" data-i18n="settings.robots.wecom.title">企业微信</span>
<span class="robot-card-desc" data-i18n="settings.robots.wecom.subtitle">HTTP 回调模式,适合企业内部应用机器人</span>
@@ -3548,7 +3548,7 @@
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
</span>
</button>
<button type="button" class="robot-card" data-robot-card="dingtalk" onclick="openRobotEditor('dingtalk')">
<button type="button" class="robot-card" data-robot-card="dingtalk" data-require-permission="robot:write" onclick="openRobotEditor('dingtalk')">
<span class="robot-card-main">
<span class="robot-card-title" data-i18n="settings.robots.dingtalk.title">钉钉</span>
<span class="robot-card-desc" data-i18n="settings.robots.dingtalk.subtitle">企业内部应用机器人,使用 Stream 长连接</span>
@@ -3558,7 +3558,7 @@
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
</span>
</button>
<button type="button" class="robot-card" data-robot-card="lark" onclick="openRobotEditor('lark')">
<button type="button" class="robot-card" data-robot-card="lark" data-require-permission="robot:write" onclick="openRobotEditor('lark')">
<span class="robot-card-main">
<span class="robot-card-title" data-i18n="settings.robots.lark.title">飞书 (Lark)</span>
<span class="robot-card-desc" data-i18n="settings.robots.lark.subtitle">飞书企业应用机器人,支持单聊与群聊 @</span>
@@ -3568,7 +3568,7 @@
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
</span>
</button>
<button type="button" class="robot-card" data-robot-card="telegram" onclick="openRobotEditor('telegram')">
<button type="button" class="robot-card" data-robot-card="telegram" data-require-permission="robot:write" onclick="openRobotEditor('telegram')">
<span class="robot-card-main">
<span class="robot-card-title" data-i18n="settings.robots.telegram.title">Telegram</span>
<span class="robot-card-desc" data-i18n="settings.robots.telegram.subtitle">Bot API 长轮询,私聊与群聊 @</span>
@@ -3578,7 +3578,7 @@
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
</span>
</button>
<button type="button" class="robot-card" data-robot-card="slack" onclick="openRobotEditor('slack')">
<button type="button" class="robot-card" data-robot-card="slack" data-require-permission="robot:write" onclick="openRobotEditor('slack')">
<span class="robot-card-main">
<span class="robot-card-title" data-i18n="settings.robots.slack.title">Slack</span>
<span class="robot-card-desc" data-i18n="settings.robots.slack.subtitle">Socket Mode,无需公网回调</span>
@@ -3588,7 +3588,7 @@
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
</span>
</button>
<button type="button" class="robot-card" data-robot-card="discord" onclick="openRobotEditor('discord')">
<button type="button" class="robot-card" data-robot-card="discord" data-require-permission="robot:write" onclick="openRobotEditor('discord')">
<span class="robot-card-main">
<span class="robot-card-title" data-i18n="settings.robots.discord.title">Discord</span>
<span class="robot-card-desc" data-i18n="settings.robots.discord.subtitle">Gateway WebSocket,私聊与服务器 @</span>
@@ -3598,7 +3598,7 @@
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
</span>
</button>
<button type="button" class="robot-card" data-robot-card="qq" onclick="openRobotEditor('qq')">
<button type="button" class="robot-card" data-robot-card="qq" data-require-permission="robot:write" onclick="openRobotEditor('qq')">
<span class="robot-card-main">
<span class="robot-card-title" data-i18n="settings.robots.qq.title">QQ 机器人</span>
<span class="robot-card-desc" data-i18n="settings.robots.qq.subtitle">QQ 开放平台 WebSocketC2C 与群 @</span>
@@ -3634,7 +3634,7 @@
</label>
</div>
<div class="robot-wechat-action-row">
<button type="button" class="btn-primary" id="robot-wechat-bind-btn" onclick="startWechatRobotBind()" data-i18n="settings.robots.wechat.bindButton">生成二维码并绑定</button>
<button type="button" class="btn-primary" data-require-permission="robot:write" id="robot-wechat-bind-btn" onclick="startWechatRobotBind()" data-i18n="settings.robots.wechat.bindButton">生成二维码并绑定</button>
<p class="robot-wechat-hint" id="robot-wechat-bind-hint" data-i18n="settings.robots.wechat.bindHint">用微信扫码确认后会自动保存并启用。</p>
</div>
<div id="robot-wechat-bound-flash" class="robot-wechat-bound-flash" hidden role="status">
@@ -3663,7 +3663,7 @@
<label for="robot-wechat-verify-code" data-i18n="settings.robots.wechat.verifyCodeLabel">手机显示的数字(仅部分账号需要)</label>
<div class="robot-wechat-verify-row">
<input type="text" id="robot-wechat-verify-code" inputmode="numeric" autocomplete="one-time-code" maxlength="8" placeholder="000000" />
<button type="button" class="btn-primary btn-sm" onclick="submitWechatVerifyCode()" data-i18n="settings.robots.wechat.verifyCodeSubmit">提交</button>
<button type="button" class="btn-primary btn-sm" data-require-permission="robot:write" onclick="submitWechatVerifyCode()" data-i18n="settings.robots.wechat.verifyCodeSubmit">提交</button>
</div>
</div>
</div>
@@ -3982,7 +3982,7 @@
</div>
<div class="settings-actions">
<button class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
<button class="btn-primary" onclick="applySettings()" data-require-permission="config:write" data-i18n="settings.apply.button">应用配置</button>
</div>
</div>
@@ -4101,8 +4101,8 @@
<span class="audit-export-caret" aria-hidden="true"></span>
</button>
<div id="audit-export-menu" class="audit-export-menu" role="menu" hidden>
<button type="button" class="audit-export-menu-item" role="menuitem" onclick="runAuditExport('json')" data-i18n="settingsAudit.exportJson">导出 JSON</button>
<button type="button" class="audit-export-menu-item" role="menuitem" onclick="runAuditExport('csv')" data-i18n="settingsAudit.exportCsv">导出 CSV</button>
<button type="button" class="audit-export-menu-item" role="menuitem" data-require-permission="audit:read" onclick="runAuditExport('json')" data-i18n="settingsAudit.exportJson">导出 JSON</button>
<button type="button" class="audit-export-menu-item" role="menuitem" data-require-permission="audit:read" onclick="runAuditExport('csv')" data-i18n="settingsAudit.exportCsv">导出 CSV</button>
</div>
</div>
</div>
@@ -4137,7 +4137,7 @@
</div>
<div class="form-actions">
<button class="btn-secondary" type="button" onclick="resetPasswordForm()" data-i18n="settingsSecurity.clear">清空</button>
<button class="btn-primary change-password-submit" type="button" onclick="changePassword()" data-i18n="settingsSecurity.changePasswordBtn">修改密码</button>
<button class="btn-primary change-password-submit" type="button" data-require-permission="auth:self" onclick="changePassword()" data-i18n="settingsSecurity.changePasswordBtn">修改密码</button>
</div>
</div>
</div>
@@ -4214,7 +4214,7 @@
<p id="rbac-selected-subtitle" class="rbac-muted" data-i18n="rbac.selectUserHint">选择一个平台用户后编辑角色、状态与资源授权。</p>
</div>
<div class="rbac-detail-actions">
<button class="btn-secondary btn-small btn-delete" type="button" id="rbac-delete-user-btn" onclick="deleteSelectedRbacUser()" disabled data-i18n="common.delete">删除</button>
<button class="btn-secondary btn-small btn-delete" data-require-permission="rbac:write" type="button" id="rbac-delete-user-btn" onclick="deleteSelectedRbacUser()" disabled data-i18n="common.delete">删除</button>
<button class="btn-primary btn-small" type="button" id="rbac-edit-user-btn" onclick="openRbacUserModalForSelected()" disabled data-i18n="common.edit">编辑</button>
</div>
</div>
@@ -4235,7 +4235,7 @@
<div class="rbac-role-save-actions">
<span id="rbac-role-unsaved" class="rbac-unsaved-indicator" hidden data-i18n="rbac.unsavedChanges">有未保存的变更</span>
<button id="rbac-role-reset-btn" class="btn-secondary btn-small" type="button" onclick="resetSelectedUserRoles()" disabled data-i18n="common.cancel">取消</button>
<button id="rbac-role-apply-btn" class="btn-primary btn-small" type="button" onclick="saveSelectedUserRoles()" disabled data-i18n="rbac.saveRoleChanges">保存角色变更</button>
<button id="rbac-role-apply-btn" class="btn-primary btn-small" data-require-permission="rbac:write" type="button" onclick="saveSelectedUserRoles()" disabled data-i18n="rbac.saveRoleChanges">保存角色变更</button>
</div>
</div>
<div id="rbac-roles-list" class="rbac-role-grid"></div>
@@ -4278,7 +4278,7 @@
<div id="rbac-pending-list" class="rbac-pending-list"></div>
<div class="rbac-pending-bar-actions">
<button type="button" class="btn-secondary btn-small" onclick="clearRbacResourceSelection()" data-i18n="rbac.clearSelection">清空选择</button>
<button type="button" class="btn-primary btn-small" onclick="createRbacAssignment()" data-i18n="rbac.confirmGrant">确认授权</button>
<button type="button" class="btn-primary btn-small" data-require-permission="rbac:write" onclick="createRbacAssignment()" data-i18n="rbac.confirmGrant">确认授权</button>
</div>
</div>
<div class="rbac-assignment-columns">
@@ -4529,7 +4529,7 @@
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeExternalMCPModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" onclick="saveExternalMCP()" data-i18n="common.save">保存</button>
<button class="btn-primary" data-require-permission="mcp:write" onclick="saveExternalMCP()" data-i18n="common.save">保存</button>
</div>
</div>
</div>
@@ -4540,7 +4540,7 @@
<div class="modal-header">
<h2 data-i18n="attackChainModal.title">攻击链可视化</h2>
<div class="modal-header-actions">
<button class="btn-primary attack-chain-action-btn" onclick="regenerateAttackChain()" data-i18n="attackChainModal.regenerateTitle" data-i18n-attr="title" data-i18n-skip-text="true" title="重新生成攻击链(包含最新对话内容)">
<button class="btn-primary attack-chain-action-btn" data-require-permission="attackchain:write" onclick="regenerateAttackChain()" data-i18n="attackChainModal.regenerateTitle" data-i18n-attr="title" data-i18n-skip-text="true" title="重新生成攻击链(包含最新对话内容)">
<span data-i18n="attackChainModal.regenerate">重新生成</span>
</button>
<button class="btn-secondary attack-chain-action-btn" onclick="exportAttackChain('png')" data-i18n="attackChainModal.exportPng" data-i18n-attr="title" title="导出为PNG">
@@ -4657,7 +4657,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn-secondary" onclick="closeChatFilesEditModal()" data-i18n="common.cancel">取消</button>
<button type="button" class="btn-primary" onclick="saveChatFilesEdit()" data-i18n="common.save">保存</button>
<button type="button" class="btn-primary" data-require-permission="files:write" onclick="saveChatFilesEdit()" data-i18n="common.save">保存</button>
</div>
</div>
</div>
@@ -4818,7 +4818,7 @@
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeSkillModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" onclick="saveSkill()" data-i18n="common.save">保存</button>
<button class="btn-primary" data-require-permission="skills:write" onclick="saveSkill()" data-i18n="common.save">保存</button>
</div>
</div>
</div>
@@ -4845,7 +4845,7 @@
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeKnowledgeItemModal()">取消</button>
<button class="btn-primary" onclick="saveKnowledgeItem()">保存</button>
<button class="btn-primary" data-require-permission="knowledge:write" onclick="saveKnowledgeItem()">保存</button>
</div>
</div>
</div>
@@ -4898,7 +4898,7 @@
</div>
<div class="batch-footer-actions">
<button class="btn-secondary" onclick="closeBatchManageModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" onclick="deleteSelectedConversations()" data-i18n="batchManageModal.deleteSelected">删除所选</button>
<button class="btn-primary" data-require-permission="chat:delete" onclick="deleteSelectedConversations()" data-i18n="batchManageModal.deleteSelected">删除所选</button>
</div>
</div>
</div>
@@ -4962,7 +4962,7 @@
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeCreateGroupModal()" data-i18n="createGroupModal.cancel">取消</button>
<button class="btn-primary" onclick="createGroup(event)" data-i18n="createGroupModal.create">创建</button>
<button class="btn-primary" data-require-permission="group:write" onclick="createGroup(event)" data-i18n="createGroupModal.create">创建</button>
</div>
</div>
</div>
@@ -5036,7 +5036,7 @@
<div id="move-to-group-submenu" class="context-submenu" style="display: none;" onmouseenter="clearSubmenuHideTimeout()" onmouseleave="hideMoveToGroupSubmenu()"></div>
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item context-menu-item-danger" onclick="deleteConversationFromContext()">
<div class="context-menu-item context-menu-item-danger" data-require-permission="chat:delete" onclick="deleteConversationFromContext()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -5059,7 +5059,7 @@
</svg>
<span id="pin-group-menu-text" data-i18n="contextMenu.pinGroup">置顶此分组</span>
</div>
<div class="context-menu-item context-menu-item-danger" onclick="deleteGroupFromContext()">
<div class="context-menu-item context-menu-item-danger" data-require-permission="group:delete" onclick="deleteGroupFromContext()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -5069,14 +5069,14 @@
<!-- 项目列表操作菜单 -->
<div id="projects-list-action-menu" class="context-menu" style="display: none;" role="menu">
<div id="projects-list-menu-edit" class="context-menu-item" onclick="editProjectFromListMenu()">
<div id="projects-list-menu-edit" class="context-menu-item" data-require-permission="project:write" onclick="editProjectFromListMenu()">
<span id="projects-list-menu-edit-text"></span>
</div>
<div id="projects-list-menu-archive" class="context-menu-item" onclick="toggleProjectArchiveFromListMenu()">
<div id="projects-list-menu-archive" class="context-menu-item" data-require-permission="project:write" onclick="toggleProjectArchiveFromListMenu()">
<span id="projects-list-menu-archive-text"></span>
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item context-menu-item-danger" onclick="deleteProjectFromListMenu()">
<div class="context-menu-item context-menu-item-danger" data-require-permission="project:delete" onclick="deleteProjectFromListMenu()">
<span id="projects-list-menu-delete-text"></span>
</div>
</div>
@@ -5154,7 +5154,7 @@
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeBatchImportModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" onclick="createBatchQueue()" data-i18n="batchImportModal.createQueue">创建队列</button>
<button class="btn-primary" data-require-permission="tasks:write" onclick="createBatchQueue()" data-i18n="batchImportModal.createQueue">创建队列</button>
</div>
</div>
</div>
@@ -5166,11 +5166,11 @@
<h2 id="batch-queue-detail-title" data-i18n="batchQueueDetailModal.title">批量任务队列详情</h2>
<div style="display: flex; align-items: center; gap: 12px;">
<div class="modal-header-actions">
<button class="btn-secondary btn-small" id="batch-queue-add-task-btn" onclick="showAddBatchTaskModal()" style="display: none;" data-i18n="batchQueueDetailModal.addTask">添加任务</button>
<button class="btn-primary btn-small" id="batch-queue-start-btn" onclick="startBatchQueue()" style="display: none;" data-i18n="batchQueueDetailModal.startExecute">开始执行</button>
<button class="btn-primary btn-small" id="batch-queue-rerun-btn" onclick="rerunBatchQueue()" style="display: none;" data-i18n="batchQueueDetailModal.rerunQueue">重跑一轮</button>
<button class="btn-secondary btn-small" id="batch-queue-pause-btn" onclick="pauseBatchQueue()" style="display: none;" data-i18n="batchQueueDetailModal.pauseQueue">暂停队列</button>
<button class="btn-secondary btn-small btn-danger" id="batch-queue-delete-btn" onclick="deleteBatchQueue()" style="display: none;" data-i18n="batchQueueDetailModal.deleteQueue">删除队列</button>
<button class="btn-secondary btn-small" data-require-permission="tasks:write" id="batch-queue-add-task-btn" onclick="showAddBatchTaskModal()" style="display: none;" data-i18n="batchQueueDetailModal.addTask">添加任务</button>
<button class="btn-primary btn-small" data-require-permission="tasks:write" id="batch-queue-start-btn" onclick="startBatchQueue()" style="display: none;" data-i18n="batchQueueDetailModal.startExecute">开始执行</button>
<button class="btn-primary btn-small" data-require-permission="tasks:write" id="batch-queue-rerun-btn" onclick="rerunBatchQueue()" style="display: none;" data-i18n="batchQueueDetailModal.rerunQueue">重跑一轮</button>
<button class="btn-secondary btn-small" data-require-permission="tasks:write" id="batch-queue-pause-btn" onclick="pauseBatchQueue()" style="display: none;" data-i18n="batchQueueDetailModal.pauseQueue">暂停队列</button>
<button class="btn-secondary btn-small btn-danger" data-require-permission="tasks:delete" id="batch-queue-delete-btn" onclick="deleteBatchQueue()" style="display: none;" data-i18n="batchQueueDetailModal.deleteQueue">删除队列</button>
</div>
<span class="modal-close" onclick="closeBatchQueueDetailModal()">&times;</span>
</div>
@@ -5194,7 +5194,7 @@
<textarea id="add-task-message" class="form-control" rows="5" data-i18n="addBatchTaskModal.taskMessagePlaceholder" data-i18n-attr="placeholder" placeholder="请输入任务消息"></textarea>
</div>
<div class="form-actions">
<button class="btn-primary" onclick="saveAddBatchTask()" data-i18n="addBatchTaskModal.add">添加</button>
<button class="btn-primary" data-require-permission="tasks:write" onclick="saveAddBatchTask()" data-i18n="addBatchTaskModal.add">添加</button>
<button class="btn-secondary" onclick="closeAddBatchTaskModal()" data-i18n="common.cancel">取消</button>
</div>
</div>
@@ -5292,7 +5292,7 @@
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeVulnerabilityModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" onclick="saveVulnerability()" data-i18n="common.save">保存</button>
<button class="btn-primary" data-require-permission="vulnerability:write" onclick="saveVulnerability()" data-i18n="common.save">保存</button>
</div>
</div>
</div>
@@ -5362,7 +5362,7 @@
<div class="modal-footer">
<button type="button" class="btn-secondary" id="webshell-test-btn" onclick="testWebshellConnection()" data-i18n="webshell.testConnectivity">测试连通性</button>
<button class="btn-secondary" onclick="closeWebshellModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" onclick="saveWebshellConnection()" data-i18n="common.save">保存</button>
<button class="btn-primary" data-require-permission="webshell:write" onclick="saveWebshellConnection()" data-i18n="common.save">保存</button>
</div>
</div>
</div>
@@ -5470,7 +5470,7 @@
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeRoleModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" onclick="saveRole()" data-i18n="common.save">保存</button>
<button class="btn-primary" data-require-permission="roles:write" onclick="saveRole()" data-i18n="common.save">保存</button>
</div>
</div>
</div>
@@ -5515,7 +5515,7 @@
</div>
<div class="modal-footer">
<button class="btn-secondary" type="button" onclick="closeRbacUserModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" type="button" onclick="saveRbacUser()" data-i18n="common.save">保存</button>
<button class="btn-primary" data-require-permission="rbac:write" type="button" onclick="saveRbacUser()" data-i18n="common.save">保存</button>
</div>
</div>
</div>
@@ -5557,7 +5557,7 @@
</div>
<div class="modal-footer">
<button class="btn-secondary" type="button" onclick="closeRbacRoleModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" id="rbac-role-save-btn" type="button" onclick="saveRbacRole()" data-i18n="common.save">保存</button>
<button class="btn-primary" data-require-permission="rbac:write" id="rbac-role-save-btn" type="button" onclick="saveRbacRole()" data-i18n="common.save">保存</button>
</div>
</div>
</div>
@@ -5587,7 +5587,7 @@
</div>
<div class="projects-modal-footer">
<button class="btn-secondary" type="button" onclick="closeProjectModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" type="button" id="project-modal-submit-btn" onclick="saveProjectModal()" data-i18n="projects.createProject">创建项目</button>
<button class="btn-primary" data-require-permission="project:write" type="button" id="project-modal-submit-btn" onclick="saveProjectModal()" data-i18n="projects.createProject">创建项目</button>
</div>
</div>
</div>
@@ -5664,7 +5664,7 @@
</div>
<div class="projects-modal-footer">
<button class="btn-secondary" type="button" onclick="closeFactModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" type="button" id="fact-modal-submit-btn" onclick="saveFactModal()" data-i18n="projects.saveFact">保存事实</button>
<button class="btn-primary" data-require-permission="project:write" type="button" id="fact-modal-submit-btn" onclick="saveFactModal()" data-i18n="projects.saveFact">保存事实</button>
</div>
</div>
</div>
@@ -5686,7 +5686,7 @@
<div class="projects-modal-footer projects-modal-footer--split">
<div class="projects-modal-footer-left">
<button class="btn-secondary btn-small" type="button" id="fact-detail-link-vuln-btn" onclick="linkFactToExistingVulnerability()" hidden data-i18n="projects.linkVulnerability">关联漏洞</button>
<button class="btn-secondary btn-small" type="button" id="fact-detail-create-vuln-btn" onclick="createVulnerabilityFromCurrentFact()" hidden data-i18n="projects.createVulnerabilityDraft">生成漏洞草稿</button>
<button class="btn-secondary btn-small" data-require-permission="vulnerability:write" type="button" id="fact-detail-create-vuln-btn" onclick="createVulnerabilityFromCurrentFact()" hidden data-i18n="projects.createVulnerabilityDraft">生成漏洞草稿</button>
</div>
<div class="projects-modal-footer-right">
<button class="btn-secondary" type="button" onclick="closeFactDetailModal()" data-i18n="common.close">关闭</button>
@@ -5729,5 +5729,6 @@
<script src="/static/js/roles.js"></script>
<script src="/static/js/rbac.js"></script>
<script src="/static/js/c2.js"></script>
<script src="/static/js/rbac-guards.js"></script>
</body>
</html>