mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-28 05:40:33 +02:00
Add files via upload
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "登录失败,请稍后重试",
|
||||
|
||||
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user