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) {