mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-13 23:47:24 +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) {
|
||||
|
||||
+115
-114
@@ -118,7 +118,7 @@
|
||||
<div id="notification-dropdown" class="notification-dropdown" style="display: none;">
|
||||
<div class="notification-dropdown-header">
|
||||
<span id="notification-dropdown-title" data-i18n="notifications.title">事件通知</span>
|
||||
<button class="notification-mark-read-btn" id="notification-mark-all-read-btn" type="button" onclick="markAllNotificationsSeen()" data-i18n="notifications.markAllRead">标记已读</button>
|
||||
<button class="notification-mark-read-btn" id="notification-mark-all-read-btn" type="button" data-require-permission="notification:write" onclick="markAllNotificationsSeen()" data-i18n="notifications.markAllRead">标记已读</button>
|
||||
</div>
|
||||
<div id="notification-list" class="notification-list">
|
||||
<div class="notification-empty" data-i18n="notifications.empty">暂无新事件</div>
|
||||
@@ -867,7 +867,7 @@
|
||||
<aside class="conversation-sidebar" id="conversation-sidebar">
|
||||
<!-- 头部一行:折叠与「新对话」并排,避免绝对定位重叠(flex 为最佳实践) -->
|
||||
<div class="sidebar-header conversation-sidebar-header">
|
||||
<button type="button" class="new-chat-btn" onclick="startNewConversation()">
|
||||
<button type="button" class="new-chat-btn" data-require-permission="chat:write" onclick="startNewConversation()">
|
||||
<span>+</span> <span data-i18n="chat.newChat">新对话</span>
|
||||
</button>
|
||||
<button type="button" class="conversation-sidebar-collapse-btn" onclick="toggleConversationSidebar()" data-i18n="chat.toggleConversationPanel" data-i18n-attr="title" data-i18n-skip-text="true" title="折叠/展开对话列表" aria-label="折叠/展开对话列表">
|
||||
@@ -904,7 +904,7 @@
|
||||
<div class="conversation-groups-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title" data-i18n="chat.conversationGroups">对话分组</span>
|
||||
<button class="add-group-btn" onclick="showCreateGroupModal()" data-i18n="chat.addGroup" data-i18n-attr="title" data-i18n-skip-text="true" title="新建分组">
|
||||
<button class="add-group-btn" data-require-permission="group:write" onclick="showCreateGroupModal()" data-i18n="chat.addGroup" data-i18n-attr="title" data-i18n-skip-text="true" title="新建分组">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
@@ -949,7 +949,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="batch-manage-btn" onclick="showBatchManageModal()" data-i18n="chat.batchManage" data-i18n-attr="title" data-i18n-skip-text="true" title="批量管理">
|
||||
<button class="batch-manage-btn" data-require-permission="chat:delete" onclick="showBatchManageModal()" data-i18n="chat.batchManage" data-i18n-attr="title" data-i18n-skip-text="true" title="批量管理">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="3" y1="12" x2="21" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="3" y1="6" x2="21" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
@@ -1061,7 +1061,7 @@
|
||||
<p class="hitl-config-hint" data-i18n="chat.hitlWhitelistHint">每行一个或逗号分隔;与 config 中全局白名单合并展示。</p>
|
||||
</div>
|
||||
<div class="hitl-config-actions">
|
||||
<button type="button" class="hitl-apply-btn" id="hitl-apply-btn" onclick="window.applyHitlSidebarConfig && window.applyHitlSidebarConfig()">
|
||||
<button type="button" class="hitl-apply-btn" data-require-permission="hitl:write" id="hitl-apply-btn" onclick="window.applyHitlSidebarConfig && window.applyHitlSidebarConfig()">
|
||||
<span data-i18n="chat.hitlApply">应用</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1086,13 +1086,13 @@
|
||||
<path d="m21 21-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="group-action-btn" onclick="editGroup()" data-i18n="chatGroup.edit" data-i18n-attr="title" title="编辑">
|
||||
<button class="group-action-btn" data-require-permission="group:write" onclick="editGroup()" data-i18n="chatGroup.edit" data-i18n-attr="title" title="编辑">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="group-action-btn delete-btn" onclick="deleteGroup()" data-i18n="chatGroup.delete" data-i18n-attr="title" title="删除">
|
||||
<button class="group-action-btn delete-btn" data-require-permission="group:delete" onclick="deleteGroup()" data-i18n="chatGroup.delete" data-i18n-attr="title" title="删除">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
@@ -1157,7 +1157,7 @@
|
||||
</div>
|
||||
<div id="chat-project-list" class="role-selection-list-main"></div>
|
||||
<div class="chat-project-panel-footer">
|
||||
<button type="button" class="role-selection-item-main chat-project-panel-create-btn" onclick="showNewProjectModalFromChat()">
|
||||
<button type="button" class="role-selection-item-main chat-project-panel-create-btn" data-require-permission="project:write" onclick="showNewProjectModalFromChat()">
|
||||
<span class="chat-project-panel-create-icon" aria-hidden="true">+</span>
|
||||
<span class="chat-project-panel-create-label" data-i18n="projects.newProject">新建项目</span>
|
||||
</button>
|
||||
@@ -1260,7 +1260,7 @@
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" class="send-btn" id="chat-send-btn" onclick="sendMessage()">
|
||||
<button type="button" class="send-btn" id="chat-send-btn" data-require-permission="chat:write" onclick="sendMessage()">
|
||||
<span data-i18n="chat.send">发送</span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
@@ -1347,7 +1347,7 @@
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" class="btn-secondary" onclick="filterHitlLogs()" data-i18n="hitl.searchApply">搜索</button>
|
||||
<button type="button" class="btn-secondary btn-delete" onclick="clearHitlLogs()" data-i18n="hitl.clearAll">清空</button>
|
||||
<button type="button" class="btn-secondary btn-delete" data-require-permission="hitl:write" onclick="clearHitlLogs()" data-i18n="hitl.clearAll">清空</button>
|
||||
</div>
|
||||
<p id="hitl-logs-retention-hint" class="hitl-logs-retention-hint" hidden></p>
|
||||
<div id="hitl-logs-batch-actions" class="monitor-batch-actions" style="display: none;">
|
||||
@@ -1357,7 +1357,7 @@
|
||||
<div class="batch-actions-buttons">
|
||||
<button type="button" class="btn-secondary" onclick="selectAllHitlLogs()" data-i18n="hitl.selectAll">全选</button>
|
||||
<button type="button" class="btn-secondary" onclick="deselectAllHitlLogs()" data-i18n="hitl.deselectAll">取消全选</button>
|
||||
<button type="button" class="btn-secondary btn-delete" onclick="batchDeleteHitlLogs()" data-i18n="hitl.batchDelete">批量删除</button>
|
||||
<button type="button" class="btn-secondary btn-delete" data-require-permission="hitl:write" onclick="batchDeleteHitlLogs()" data-i18n="hitl.batchDelete">批量删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="hitl-logs-table-wrap" class="hitl-logs-table-wrap">
|
||||
@@ -1372,7 +1372,7 @@
|
||||
<span class="hitl-page-strategy-label" data-i18n="hitl.strategyLabel">审计策略</span>
|
||||
<div class="hitl-page-strategy-actions">
|
||||
<button type="button" class="btn-link" id="hitl-strategy-reset-btn" onclick="resetHitlAuditStrategy()" data-i18n="hitl.strategyReset">恢复默认</button>
|
||||
<button type="button" class="btn-secondary" id="hitl-strategy-save-btn" onclick="saveHitlAuditStrategy()" data-i18n="common.save">保存</button>
|
||||
<button type="button" class="btn-secondary" data-require-permission="hitl:write" id="hitl-strategy-save-btn" onclick="saveHitlAuditStrategy()" data-i18n="common.save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hitl-strategy-subtabs" role="tablist" aria-label="Audit strategy mode">
|
||||
@@ -1391,7 +1391,7 @@
|
||||
<div class="hitl-page-whitelist-bar" id="hitl-page-whitelist-bar">
|
||||
<div class="hitl-page-whitelist-header">
|
||||
<span class="hitl-page-whitelist-label" data-i18n="hitl.whitelistLabel">免审批工具白名单</span>
|
||||
<button type="button" class="btn-secondary" id="hitl-page-whitelist-save-btn" onclick="saveHitlPageWhitelist()" data-i18n="common.save">保存</button>
|
||||
<button type="button" class="btn-secondary" data-require-permission="hitl:write" id="hitl-page-whitelist-save-btn" onclick="saveHitlPageWhitelist()" data-i18n="common.save">保存</button>
|
||||
</div>
|
||||
<p class="hitl-page-whitelist-hint" data-i18n="hitl.whitelistHint">每行一个或逗号分隔;保存后写入 config.yaml 全局白名单并立即生效(与聊天侧栏同步展示)。</p>
|
||||
<textarea id="hitl-page-sensitive-tools" class="hitl-page-whitelist-textarea" rows="6" spellcheck="false" autocomplete="off" data-i18n="chat.hitlWhitelistPlaceholder" data-i18n-attr="placeholder" placeholder=""></textarea>
|
||||
@@ -1499,7 +1499,7 @@
|
||||
<div class="batch-actions-buttons">
|
||||
<button class="btn-secondary" onclick="selectAllExecutions()" data-i18n="mcp.selectAll">全选</button>
|
||||
<button class="btn-secondary" onclick="deselectAllExecutions()" data-i18n="mcpMonitor.deselectAll">取消全选</button>
|
||||
<button class="btn-secondary btn-delete" onclick="batchDeleteExecutions()" data-i18n="mcp.deleteSelected">批量删除</button>
|
||||
<button class="btn-secondary btn-delete" data-require-permission="monitor:delete" onclick="batchDeleteExecutions()" data-i18n="mcp.deleteSelected">批量删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="monitor-executions" class="monitor-table-container">
|
||||
@@ -1524,7 +1524,7 @@
|
||||
<div class="settings-section mcp-management-panel mcp-tools-panel" style="margin-bottom: 32px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<h3 style="margin: 0;" data-i18n="mcp.toolConfig">MCP 工具配置</h3>
|
||||
<button class="btn-primary" onclick="saveToolsConfig()" data-i18n="mcp.saveToolConfig">保存工具配置</button>
|
||||
<button class="btn-primary" data-require-permission="config:write" onclick="saveToolsConfig()" data-i18n="mcp.saveToolConfig">保存工具配置</button>
|
||||
</div>
|
||||
<div class="tools-controls mcp-panel-body">
|
||||
<div class="tools-actions">
|
||||
@@ -1549,7 +1549,7 @@
|
||||
<div class="settings-section mcp-management-panel mcp-external-panel">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<h3 style="margin: 0;" data-i18n="mcp.externalConfig">外部 MCP 配置</h3>
|
||||
<button class="btn-primary" onclick="showAddExternalMCPModal()" data-i18n="mcp.addExternalMCP">添加外部MCP</button>
|
||||
<button class="btn-primary" data-require-permission="mcp:write" onclick="showAddExternalMCPModal()" data-i18n="mcp.addExternalMCP">添加外部MCP</button>
|
||||
</div>
|
||||
<div class="external-mcp-controls mcp-panel-body">
|
||||
<div class="external-mcp-actions">
|
||||
@@ -1568,9 +1568,9 @@
|
||||
<h2 data-i18n="knowledge.title">知识管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-secondary" onclick="refreshKnowledgeBase()" data-i18n="common.refresh">刷新</button>
|
||||
<button class="btn-secondary" onclick="buildKnowledgeIndex()" data-i18n="knowledge.buildIndex">构建索引</button>
|
||||
<button class="btn-secondary" onclick="rebuildKnowledgeIndexFull()" data-i18n="knowledge.rebuildIndexFull">全量重建</button>
|
||||
<button class="btn-primary" onclick="showAddKnowledgeItemModal()" data-i18n="knowledge.addKnowledge">添加知识</button>
|
||||
<button class="btn-secondary" data-require-permission="knowledge:write" onclick="buildKnowledgeIndex()" data-i18n="knowledge.buildIndex">构建索引</button>
|
||||
<button class="btn-secondary" data-require-permission="knowledge:write" onclick="rebuildKnowledgeIndexFull()" data-i18n="knowledge.rebuildIndexFull">全量重建</button>
|
||||
<button class="btn-primary" data-require-permission="knowledge:write" onclick="showAddKnowledgeItemModal()" data-i18n="knowledge.addKnowledge">添加知识</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
@@ -1673,7 +1673,7 @@
|
||||
<h2 data-i18n="infoCollectPage.title">信息收集</h2>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-secondary" onclick="resetFofaForm()" data-i18n="infoCollectPage.reset">重置</button>
|
||||
<button class="btn-primary" onclick="submitFofaSearch()" data-i18n="infoCollectPage.confirm">确定</button>
|
||||
<button class="btn-primary" data-require-permission="fofa:execute" onclick="submitFofaSearch()" data-i18n="infoCollectPage.confirm">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
@@ -1737,10 +1737,10 @@
|
||||
<div class="info-collect-results-toolbar" aria-label="结果工具条" data-i18n="infoCollectPage.resultsToolbarAria" data-i18n-attr="aria-label" data-i18n-skip-text="true">
|
||||
<div class="info-collect-selected" id="fofa-selected-meta" data-i18n="infoCollectPage.selectedRowsZero">已选择 0 条</div>
|
||||
<button class="btn-secondary btn-small" type="button" onclick="toggleFofaColumnsPanel()" data-i18n="infoCollectPage.showHideColumns" data-i18n-attr="title" title="显示/隐藏字段"><span data-i18n="infoCollectPage.columns">列</span></button>
|
||||
<button class="btn-secondary btn-small" type="button" onclick="exportFofaResults('csv')" data-i18n="infoCollectPage.exportCsvTitle" data-i18n-attr="title" title="导出当前结果为 CSV(UTF-8,兼容中文)"><span data-i18n="infoCollectPage.exportCsv">导出 CSV</span></button>
|
||||
<button class="btn-secondary btn-small" type="button" onclick="exportFofaResults('json')" data-i18n="infoCollectPage.exportJsonTitle" data-i18n-attr="title" title="导出当前结果为 JSON"><span data-i18n="infoCollectPage.exportJson">导出 JSON</span></button>
|
||||
<button class="btn-secondary btn-small" type="button" onclick="exportFofaResults('xlsx')" data-i18n="infoCollectPage.exportXlsxTitle" data-i18n-attr="title" title="导出当前结果为 Excel"><span data-i18n="infoCollectPage.exportXlsx">导出 XLSX</span></button>
|
||||
<button class="btn-primary btn-small" type="button" onclick="batchScanSelectedFofaRows()" data-i18n="infoCollectPage.batchScanTitle" data-i18n-attr="title" title="将所选行创建为批量任务队列"><span data-i18n="infoCollectPage.batchScan">批量扫描</span></button>
|
||||
<button class="btn-secondary btn-small" type="button" data-require-permission="fofa:execute" onclick="exportFofaResults('csv')" data-i18n="infoCollectPage.exportCsvTitle" data-i18n-attr="title" title="导出当前结果为 CSV(UTF-8,兼容中文)"><span data-i18n="infoCollectPage.exportCsv">导出 CSV</span></button>
|
||||
<button class="btn-secondary btn-small" type="button" data-require-permission="fofa:execute" onclick="exportFofaResults('json')" data-i18n="infoCollectPage.exportJsonTitle" data-i18n-attr="title" title="导出当前结果为 JSON"><span data-i18n="infoCollectPage.exportJson">导出 JSON</span></button>
|
||||
<button class="btn-secondary btn-small" type="button" data-require-permission="fofa:execute" onclick="exportFofaResults('xlsx')" data-i18n="infoCollectPage.exportXlsxTitle" data-i18n-attr="title" title="导出当前结果为 Excel"><span data-i18n="infoCollectPage.exportXlsx">导出 XLSX</span></button>
|
||||
<button class="btn-primary btn-small" data-require-permission="fofa:execute" type="button" onclick="batchScanSelectedFofaRows()" data-i18n="infoCollectPage.batchScanTitle" data-i18n-attr="title" title="将所选行创建为批量任务队列"><span data-i18n="infoCollectPage.batchScan">批量扫描</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-collect-results-table-wrap">
|
||||
@@ -1774,7 +1774,7 @@
|
||||
<div class="page-header-actions">
|
||||
<label class="projects-show-archived-label"><input type="checkbox" id="projects-show-archived" onchange="loadProjectsList()"> <span data-i18n="projects.showArchived">显示已归档</span></label>
|
||||
<button class="btn-secondary" type="button" onclick="loadProjectsList()" data-i18n="common.refresh">刷新</button>
|
||||
<button class="btn-primary" type="button" onclick="showNewProjectModal()" data-i18n="projects.newProjectCta">+ 新建项目</button>
|
||||
<button class="btn-primary" type="button" data-require-permission="project:write" onclick="showNewProjectModal()" data-i18n="projects.newProjectCta">+ 新建项目</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content projects-page-layout">
|
||||
@@ -1800,7 +1800,7 @@
|
||||
</div>
|
||||
<h3 data-i18n="projects.selectOrCreateTitle">选择或创建项目</h3>
|
||||
<p data-i18n="projects.selectOrCreateHint">项目用于跨对话共享「事实黑板」:目标、环境、认证等信息会在绑定项目的对话中自动注入。</p>
|
||||
<button class="btn-primary" type="button" onclick="showNewProjectModal()" data-i18n="projects.createFirstProject">创建第一个项目</button>
|
||||
<button class="btn-primary" type="button" data-require-permission="project:write" onclick="showNewProjectModal()" data-i18n="projects.createFirstProject">创建第一个项目</button>
|
||||
</div>
|
||||
<div class="projects-detail-inner" id="projects-detail-inner" hidden>
|
||||
<header class="projects-detail-header">
|
||||
@@ -1825,7 +1825,7 @@
|
||||
<span id="projects-detail-meta-time" class="projects-detail-meta-time"></span>
|
||||
</span>
|
||||
<button type="button" class="btn-secondary btn-small" onclick="openVulnerabilitiesForProject()" data-i18n="projects.vulnerabilityManagement">漏洞管理</button>
|
||||
<button type="button" class="btn-primary btn-small" onclick="showAddFactModal()" data-i18n="projects.addFactCta">+ 添加事实</button>
|
||||
<button type="button" class="btn-primary btn-small" data-require-permission="project:write" onclick="showAddFactModal()" data-i18n="projects.addFactCta">+ 添加事实</button>
|
||||
</div>
|
||||
</header>
|
||||
<nav class="projects-tabs" role="tablist">
|
||||
@@ -1930,7 +1930,7 @@
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
||||
<span data-i18n="projects.graphCenter">居中</span>
|
||||
</button>
|
||||
<button type="button" class="projects-graph-action-btn projects-graph-action-btn--connect" id="project-graph-connect-btn" onclick="toggleProjectFactGraphConnectMode()" data-i18n="projects.graphConnect">连边</button>
|
||||
<button type="button" class="projects-graph-action-btn projects-graph-action-btn--connect" data-require-permission="project:write" id="project-graph-connect-btn" onclick="toggleProjectFactGraphConnectMode()" data-i18n="projects.graphConnect">连边</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1954,7 +1954,7 @@
|
||||
</div>
|
||||
<div class="project-fact-graph-sidebar-actions">
|
||||
<button type="button" class="btn-primary btn-small" id="project-fact-graph-detail-btn" onclick="openSelectedGraphFactDetail()" data-i18n="projects.details">详情</button>
|
||||
<button type="button" class="btn-secondary btn-small" id="project-fact-graph-edit-btn" onclick="editSelectedGraphFact()" data-i18n="common.edit">编辑</button>
|
||||
<button type="button" class="btn-secondary btn-small" data-require-permission="project:write" id="project-fact-graph-edit-btn" onclick="editSelectedGraphFact()" data-i18n="common.edit">编辑</button>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
@@ -2135,14 +2135,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-settings-danger-actions">
|
||||
<button class="btn-secondary btn-small" type="button" onclick="archiveCurrentProject()" data-i18n="projects.archiveRestore">归档 / 恢复</button>
|
||||
<button class="btn-secondary btn-small btn-danger-outline" type="button" onclick="deleteCurrentProject()" data-i18n="projects.deleteProject">删除项目</button>
|
||||
<button class="btn-secondary btn-small" data-require-permission="project:write" type="button" onclick="archiveCurrentProject()" data-i18n="projects.archiveRestore">归档 / 恢复</button>
|
||||
<button class="btn-secondary btn-small btn-danger-outline" data-require-permission="project:delete" type="button" onclick="deleteCurrentProject()" data-i18n="projects.deleteProject">删除项目</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<footer class="projects-settings-footer">
|
||||
<span class="projects-settings-footer-hint" data-i18n="projects.saveChangesHint">修改后请点击保存以同步到服务器</span>
|
||||
<button class="btn-primary" type="button" onclick="saveProjectSettings()">
|
||||
<button class="btn-primary" data-require-permission="project:write" type="button" onclick="saveProjectSettings()">
|
||||
<span data-i18n="projects.saveSettings">保存更改</span>
|
||||
</button>
|
||||
</footer>
|
||||
@@ -2156,10 +2156,10 @@
|
||||
<div class="page-header">
|
||||
<h2 data-i18n="vulnerability.title">漏洞管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-secondary" onclick="exportVulnerabilityReports()" data-i18n="vulnerabilityPage.batchExport">批量导出</button>
|
||||
<button class="btn-secondary btn-delete" onclick="batchDeleteVulnerabilityReports()" data-i18n="vulnerabilityPage.batchDelete">批量删除</button>
|
||||
<button class="btn-secondary" data-require-permission="vulnerability:read" onclick="exportVulnerabilityReports()" data-i18n="vulnerabilityPage.batchExport">批量导出</button>
|
||||
<button class="btn-secondary btn-delete" data-require-permission="vulnerability:delete" onclick="batchDeleteVulnerabilityReports()" data-i18n="vulnerabilityPage.batchDelete">批量删除</button>
|
||||
<button class="btn-secondary" onclick="refreshVulnerabilities()" data-i18n="common.refresh">刷新</button>
|
||||
<button class="btn-primary" onclick="showAddVulnerabilityModal()" data-i18n="vulnerability.addVuln">添加漏洞</button>
|
||||
<button class="btn-primary" data-require-permission="vulnerability:write" onclick="showAddVulnerabilityModal()" data-i18n="vulnerability.addVuln">添加漏洞</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
@@ -2322,7 +2322,7 @@
|
||||
<div class="page-header">
|
||||
<h2 data-i18n="webshell.title">WebShell 管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-primary" onclick="showAddWebshellModal()" data-i18n="webshell.addConnection">添加连接</button>
|
||||
<button class="btn-primary" data-require-permission="webshell:write" onclick="showAddWebshellModal()" data-i18n="webshell.addConnection">添加连接</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content webshell-page-content">
|
||||
@@ -2359,7 +2359,7 @@
|
||||
<div class="page-header">
|
||||
<h2 data-i18n="chatFilesPage.title">文件管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button type="button" class="btn-primary" id="chat-files-header-upload-btn" onclick="chatFilesOpenUploadPicker()" data-i18n="chatFilesPage.upload">上传文件</button>
|
||||
<button type="button" class="btn-primary" id="chat-files-header-upload-btn" data-require-permission="files:write" onclick="chatFilesOpenUploadPicker()" data-i18n="chatFilesPage.upload">上传文件</button>
|
||||
<input type="file" id="chat-files-upload-input" style="display:none" multiple onchange="onChatFilesUploadPick(event)" />
|
||||
<button type="button" class="btn-secondary" id="chat-files-refresh-btn" onclick="loadChatFilesPage()" data-i18n="common.refresh">刷新</button>
|
||||
</div>
|
||||
@@ -2401,7 +2401,7 @@
|
||||
<div class="page-header">
|
||||
<h2 data-i18n="tasks.title">任务管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-primary" onclick="showBatchImportModal()" data-i18n="tasks.newTask">新建任务</button>
|
||||
<button class="btn-primary" data-require-permission="tasks:write" onclick="showBatchImportModal()" data-i18n="tasks.newTask">新建任务</button>
|
||||
<label class="auto-refresh-toggle">
|
||||
<input type="checkbox" id="tasks-auto-refresh" checked onchange="toggleTasksAutoRefresh(this.checked)">
|
||||
<span data-i18n="tasks.autoRefresh">自动刷新</span>
|
||||
@@ -2444,7 +2444,7 @@
|
||||
<div class="page-header">
|
||||
<h2 data-i18n="c2.listeners.title">监听器管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-primary" onclick="C2.showCreateListenerModal()" data-i18n="c2.listeners.headerCreateBtn">+ 创建监听器</button>
|
||||
<button class="btn-primary" data-require-permission="c2:write" onclick="C2.showCreateListenerModal()" data-i18n="c2.listeners.headerCreateBtn">+ 创建监听器</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
@@ -2457,8 +2457,8 @@
|
||||
<div class="page-header">
|
||||
<h2 data-i18n="c2.sessions.title">会话管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button type="button" class="btn-danger" id="c2-sessions-batch-delete" disabled onclick="C2.deleteSelectedSessions()"><span data-i18n="c2.sessions.batchDelete">批量删除</span></button>
|
||||
<button type="button" class="btn-secondary" id="c2-sessions-delete-filtered" disabled onclick="C2.deleteFilteredSessions()"><span data-i18n="c2.sessions.deleteFiltered">删除筛选结果</span></button>
|
||||
<button type="button" class="btn-danger" id="c2-sessions-batch-delete" data-require-permission="c2:delete" disabled onclick="C2.deleteSelectedSessions()"><span data-i18n="c2.sessions.batchDelete">批量删除</span></button>
|
||||
<button type="button" class="btn-secondary" id="c2-sessions-delete-filtered" data-require-permission="c2:delete" disabled onclick="C2.deleteFilteredSessions()"><span data-i18n="c2.sessions.deleteFiltered">删除筛选结果</span></button>
|
||||
<button class="btn-secondary" onclick="C2.loadSessions()"><span data-i18n="common.refresh">刷新</span></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2478,7 +2478,7 @@
|
||||
<div class="page-header">
|
||||
<h2 data-i18n="c2.tasks.title">任务管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button type="button" class="btn-danger" id="c2-tasks-batch-delete" disabled onclick="C2.deleteSelectedTasks()"><span data-i18n="c2.tasks.batchDelete">批量删除</span></button>
|
||||
<button type="button" class="btn-danger" id="c2-tasks-batch-delete" data-require-permission="c2:delete" disabled onclick="C2.deleteSelectedTasks()"><span data-i18n="c2.tasks.batchDelete">批量删除</span></button>
|
||||
<button type="button" class="btn-secondary" onclick="C2.loadTasks()"><span data-i18n="common.refresh">刷新</span></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2521,7 +2521,7 @@
|
||||
<label data-i18n="c2.payloads.hostOptional">回连地址 (可选)</label>
|
||||
<input type="text" id="c2-payload-host" class="form-control" data-i18n="c2.payloads.placeholderListenerHost" data-i18n-attr="placeholder" placeholder="留空则使用监听器地址">
|
||||
</div>
|
||||
<button type="button" id="c2-generate-oneliner-btn" class="btn-primary" onclick="C2.generateOneliner()" style="width:100%;" data-i18n="c2.payloads.generateOnelinerBtn">生成 Oneliner</button>
|
||||
<button type="button" id="c2-generate-oneliner-btn" class="btn-primary" data-require-permission="c2:write" onclick="C2.generateOneliner()" style="width:100%;" data-i18n="c2.payloads.generateOnelinerBtn">生成 Oneliner</button>
|
||||
<pre id="c2-oneliner-output" class="c2-oneliner-output" style="display:none;" onclick="C2.copyOneliner()" data-i18n="c2.payloads.clickToCopyTitle" data-i18n-attr="title" data-i18n-skip-text="true" title="点击复制"></pre>
|
||||
</div>
|
||||
<div class="c2-payload-card">
|
||||
@@ -2559,7 +2559,7 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button id="c2-build-btn" type="button" class="btn-primary" onclick="C2.buildBeacon()" style="width:100%;" data-i18n="c2.payloads.buildBeaconBtn">构建 Beacon</button>
|
||||
<button id="c2-build-btn" type="button" class="btn-primary" data-require-permission="c2:write" onclick="C2.buildBeacon()" style="width:100%;" data-i18n="c2.payloads.buildBeaconBtn">构建 Beacon</button>
|
||||
<div id="c2-build-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2571,7 +2571,7 @@
|
||||
<div class="page-header">
|
||||
<h2 data-i18n="c2.events.title">事件审计</h2>
|
||||
<div class="page-header-actions">
|
||||
<button type="button" class="btn-danger" id="c2-events-batch-delete" disabled onclick="C2.deleteSelectedEvents()"><span data-i18n="c2.events.batchDelete">批量删除</span></button>
|
||||
<button type="button" class="btn-danger" id="c2-events-batch-delete" data-require-permission="c2:delete" disabled onclick="C2.deleteSelectedEvents()"><span data-i18n="c2.events.batchDelete">批量删除</span></button>
|
||||
<button type="button" class="btn-secondary" onclick="C2.loadEvents()"><span data-i18n="common.refresh">刷新</span></button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2592,7 +2592,7 @@
|
||||
<div class="page-header">
|
||||
<h2 data-i18n="c2.profiles.title">流量伪装</h2>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-primary" onclick="C2.showCreateProfileModal()" data-i18n="c2.profiles.createBtn">+ 创建 Profile</button>
|
||||
<button class="btn-primary" data-require-permission="c2:write" onclick="C2.showCreateProfileModal()" data-i18n="c2.profiles.createBtn">+ 创建 Profile</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
@@ -2631,13 +2631,13 @@
|
||||
<h3 data-i18n="workflows.nodeLibrary">节点库</h3>
|
||||
</div>
|
||||
<div class="workflow-node-palette">
|
||||
<button type="button" draggable="true" data-node-type="start" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('start')" data-i18n="workflows.nodes.start">开始</button>
|
||||
<button type="button" draggable="true" data-node-type="tool" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('tool')" data-i18n="workflows.nodes.tool">工具</button>
|
||||
<button type="button" draggable="true" data-node-type="agent" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('agent')" data-i18n="workflows.nodes.agent">Agent</button>
|
||||
<button type="button" draggable="true" data-node-type="condition" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('condition')" data-i18n="workflows.nodes.condition">条件</button>
|
||||
<button type="button" draggable="true" data-node-type="hitl" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('hitl')" data-i18n="workflows.nodes.hitl">审批</button>
|
||||
<button type="button" draggable="true" data-node-type="output" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('output')" data-i18n="workflows.nodes.output">输出</button>
|
||||
<button type="button" draggable="true" data-node-type="end" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('end')" data-i18n="workflows.nodes.end">结束</button>
|
||||
<button type="button" draggable="true" data-require-permission="workflow:write" data-node-type="start" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('start')" data-i18n="workflows.nodes.start">开始</button>
|
||||
<button type="button" draggable="true" data-require-permission="workflow:write" data-node-type="tool" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('tool')" data-i18n="workflows.nodes.tool">工具</button>
|
||||
<button type="button" draggable="true" data-require-permission="workflow:write" data-node-type="agent" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('agent')" data-i18n="workflows.nodes.agent">Agent</button>
|
||||
<button type="button" draggable="true" data-require-permission="workflow:write" data-node-type="condition" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('condition')" data-i18n="workflows.nodes.condition">条件</button>
|
||||
<button type="button" draggable="true" data-require-permission="workflow:write" data-node-type="hitl" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('hitl')" data-i18n="workflows.nodes.hitl">审批</button>
|
||||
<button type="button" draggable="true" data-require-permission="workflow:write" data-node-type="output" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('output')" data-i18n="workflows.nodes.output">输出</button>
|
||||
<button type="button" draggable="true" data-require-permission="workflow:write" data-node-type="end" ondragstart="workflowPaletteDragStart(event)" onclick="addWorkflowNodeFromPalette('end')" data-i18n="workflows.nodes.end">结束</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -2650,12 +2650,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-toolbar">
|
||||
<button class="btn-secondary btn-small" type="button" onclick="toggleWorkflowConnectMode()" id="workflow-connect-btn" data-i18n="workflows.connect">连线</button>
|
||||
<button class="btn-secondary btn-small" type="button" onclick="deleteWorkflowSelection()" data-i18n="workflows.deleteSelected">删除选中</button>
|
||||
<button class="btn-secondary btn-small" data-require-permission="workflow:write" type="button" onclick="toggleWorkflowConnectMode()" id="workflow-connect-btn" data-i18n="workflows.connect">连线</button>
|
||||
<button class="btn-secondary btn-small" data-require-permission="workflow:delete" type="button" onclick="deleteWorkflowSelection()" data-i18n="workflows.deleteSelected">删除选中</button>
|
||||
<button class="btn-secondary btn-small" type="button" onclick="layoutWorkflowGraph()" data-i18n="workflows.autoLayout">自动布局</button>
|
||||
<button class="btn-secondary btn-small" type="button" onclick="dryRunWorkflowDraft()" data-i18n="workflows.dryRun">试运行</button>
|
||||
<button class="btn-secondary btn-small" onclick="deleteCurrentWorkflow()" data-i18n="common.delete">删除</button>
|
||||
<button class="btn-primary btn-small" onclick="saveWorkflowDraft()" data-i18n="common.save">保存</button>
|
||||
<button class="btn-secondary btn-small" data-require-permission="workflow:execute" type="button" onclick="dryRunWorkflowDraft()" data-i18n="workflows.dryRun">试运行</button>
|
||||
<button class="btn-secondary btn-small" data-require-permission="workflow:delete" onclick="deleteCurrentWorkflow()" data-i18n="common.delete">删除</button>
|
||||
<button class="btn-primary btn-small" data-require-permission="workflow:write" onclick="saveWorkflowDraft()" data-i18n="common.save">保存</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="workflow-canvas-wrap" ondragover="workflowCanvasDragOver(event)" ondrop="workflowCanvasDrop(event)">
|
||||
@@ -2666,7 +2666,7 @@
|
||||
<aside class="workflow-properties">
|
||||
<div class="workflow-panel-header">
|
||||
<h3 id="workflow-property-title" data-i18n="workflows.properties">属性</h3>
|
||||
<button type="button" id="workflow-property-delete-btn" class="btn-secondary btn-small" onclick="deleteWorkflowSelection()" hidden data-i18n="common.delete">删除</button>
|
||||
<button type="button" id="workflow-property-delete-btn" class="btn-secondary btn-small" data-require-permission="workflow:delete" onclick="deleteWorkflowSelection()" hidden data-i18n="common.delete">删除</button>
|
||||
</div>
|
||||
<div id="workflow-property-empty" class="workflow-property-empty" data-i18n="workflows.propertyEmpty">选择一个节点或连线后编辑属性</div>
|
||||
<div id="workflow-property-form" class="workflow-property-form" hidden>
|
||||
@@ -2689,7 +2689,7 @@
|
||||
<div id="workflow-typed-config" class="workflow-typed-config"></div>
|
||||
<div class="workflow-custom-fields-head">
|
||||
<span data-i18n="workflows.customFields">自定义字段</span>
|
||||
<button type="button" class="btn-secondary btn-small" onclick="addWorkflowCustomField()" data-i18n="workflows.addField">添加字段</button>
|
||||
<button type="button" class="btn-secondary btn-small" data-require-permission="workflow:write" onclick="addWorkflowCustomField()" data-i18n="workflows.addField">添加字段</button>
|
||||
</div>
|
||||
<div id="workflow-custom-fields" class="workflow-custom-fields"></div>
|
||||
</div>
|
||||
@@ -2714,7 +2714,7 @@
|
||||
<h2 data-i18n="roles.title">角色管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-secondary" onclick="refreshRoles()" data-i18n="common.refresh">刷新</button>
|
||||
<button class="btn-primary" onclick="showAddRoleModal()" data-i18n="roles.createRole">创建角色</button>
|
||||
<button class="btn-primary" data-require-permission="roles:write" onclick="showAddRoleModal()" data-i18n="roles.createRole">创建角色</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
@@ -2774,7 +2774,7 @@
|
||||
<h2 data-i18n="skills.title">Skills管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-secondary" onclick="refreshSkills()" data-i18n="common.refresh">刷新</button>
|
||||
<button class="btn-primary" onclick="showAddSkillModal()" data-i18n="skills.createSkill">创建Skill</button>
|
||||
<button class="btn-primary" data-require-permission="skills:write" onclick="showAddSkillModal()" data-i18n="skills.createSkill">创建Skill</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content page-content-with-pagination">
|
||||
@@ -2820,7 +2820,7 @@
|
||||
<h2 data-i18n="agentsPage.title">Agent 管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button type="button" class="btn-secondary" onclick="loadMarkdownAgents()" data-i18n="common.refresh">刷新</button>
|
||||
<button type="button" class="btn-primary" onclick="showAddMarkdownAgentModal()" data-i18n="agentsPage.create">新建 Agent</button>
|
||||
<button type="button" class="btn-primary" data-require-permission="agents:write" onclick="showAddMarkdownAgentModal()" data-i18n="agentsPage.create">新建 Agent</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
@@ -2884,7 +2884,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-secondary" onclick="closeMarkdownAgentModal()" data-i18n="common.cancel">取消</button>
|
||||
<button type="button" class="btn-primary" onclick="saveMarkdownAgent()" data-i18n="common.save">保存</button>
|
||||
<button type="button" class="btn-primary" data-require-permission="agents:write" onclick="saveMarkdownAgent()" data-i18n="common.save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3164,7 +3164,7 @@
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
|
||||
<button class="btn-primary" onclick="applySettings()" data-require-permission="config:write" data-i18n="settings.apply.button">应用配置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3251,7 +3251,7 @@
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button type="button" class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
|
||||
<button type="button" class="btn-primary" onclick="applySettings()" data-require-permission="config:write" data-i18n="settings.apply.button">应用配置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3283,7 +3283,7 @@
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button type="button" class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
|
||||
<button type="button" class="btn-primary" onclick="applySettings()" data-require-permission="config:write" data-i18n="settings.apply.button">应用配置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3484,7 +3484,7 @@
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
|
||||
<button class="btn-primary" onclick="applySettings()" data-require-permission="config:write" data-i18n="settings.apply.button">应用配置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3507,7 +3507,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-actions">
|
||||
<button class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
|
||||
<button class="btn-primary" onclick="applySettings()" data-require-permission="config:write" data-i18n="settings.apply.button">应用配置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3524,11 +3524,11 @@
|
||||
<h4 data-i18n="settings.robots.managerTitle">机器人管理</h4>
|
||||
<p data-i18n="settings.robots.managerDesc">先选择机器人类型,再进入对应配置;已配置的平台会显示连接状态。</p>
|
||||
</div>
|
||||
<button type="button" class="btn-primary" onclick="openRobotCreateModal()" data-i18n="settings.robots.newBot">新建机器人</button>
|
||||
<button type="button" class="btn-primary" data-require-permission="robot:write" onclick="openRobotCreateModal()" data-i18n="settings.robots.newBot">新建机器人</button>
|
||||
</div>
|
||||
|
||||
<div class="robot-card-grid">
|
||||
<button type="button" class="robot-card" data-robot-card="wechat" onclick="openRobotEditor('wechat')">
|
||||
<button type="button" class="robot-card" data-robot-card="wechat" data-require-permission="robot:write" onclick="openRobotEditor('wechat')">
|
||||
<span class="robot-card-main">
|
||||
<span class="robot-card-title" data-i18n="settings.robots.wechat.title">微信 / iLink</span>
|
||||
<span class="robot-card-desc" data-i18n="settings.robots.wechat.subtitle">扫码绑定个人微信,在手机端直接与 CyberStrikeAI 对话</span>
|
||||
@@ -3538,7 +3538,7 @@
|
||||
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="robot-card" data-robot-card="wecom" onclick="openRobotEditor('wecom')">
|
||||
<button type="button" class="robot-card" data-robot-card="wecom" data-require-permission="robot:write" onclick="openRobotEditor('wecom')">
|
||||
<span class="robot-card-main">
|
||||
<span class="robot-card-title" data-i18n="settings.robots.wecom.title">企业微信</span>
|
||||
<span class="robot-card-desc" data-i18n="settings.robots.wecom.subtitle">HTTP 回调模式,适合企业内部应用机器人</span>
|
||||
@@ -3548,7 +3548,7 @@
|
||||
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="robot-card" data-robot-card="dingtalk" onclick="openRobotEditor('dingtalk')">
|
||||
<button type="button" class="robot-card" data-robot-card="dingtalk" data-require-permission="robot:write" onclick="openRobotEditor('dingtalk')">
|
||||
<span class="robot-card-main">
|
||||
<span class="robot-card-title" data-i18n="settings.robots.dingtalk.title">钉钉</span>
|
||||
<span class="robot-card-desc" data-i18n="settings.robots.dingtalk.subtitle">企业内部应用机器人,使用 Stream 长连接</span>
|
||||
@@ -3558,7 +3558,7 @@
|
||||
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="robot-card" data-robot-card="lark" onclick="openRobotEditor('lark')">
|
||||
<button type="button" class="robot-card" data-robot-card="lark" data-require-permission="robot:write" onclick="openRobotEditor('lark')">
|
||||
<span class="robot-card-main">
|
||||
<span class="robot-card-title" data-i18n="settings.robots.lark.title">飞书 (Lark)</span>
|
||||
<span class="robot-card-desc" data-i18n="settings.robots.lark.subtitle">飞书企业应用机器人,支持单聊与群聊 @</span>
|
||||
@@ -3568,7 +3568,7 @@
|
||||
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="robot-card" data-robot-card="telegram" onclick="openRobotEditor('telegram')">
|
||||
<button type="button" class="robot-card" data-robot-card="telegram" data-require-permission="robot:write" onclick="openRobotEditor('telegram')">
|
||||
<span class="robot-card-main">
|
||||
<span class="robot-card-title" data-i18n="settings.robots.telegram.title">Telegram</span>
|
||||
<span class="robot-card-desc" data-i18n="settings.robots.telegram.subtitle">Bot API 长轮询,私聊与群聊 @</span>
|
||||
@@ -3578,7 +3578,7 @@
|
||||
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="robot-card" data-robot-card="slack" onclick="openRobotEditor('slack')">
|
||||
<button type="button" class="robot-card" data-robot-card="slack" data-require-permission="robot:write" onclick="openRobotEditor('slack')">
|
||||
<span class="robot-card-main">
|
||||
<span class="robot-card-title" data-i18n="settings.robots.slack.title">Slack</span>
|
||||
<span class="robot-card-desc" data-i18n="settings.robots.slack.subtitle">Socket Mode,无需公网回调</span>
|
||||
@@ -3588,7 +3588,7 @@
|
||||
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="robot-card" data-robot-card="discord" onclick="openRobotEditor('discord')">
|
||||
<button type="button" class="robot-card" data-robot-card="discord" data-require-permission="robot:write" onclick="openRobotEditor('discord')">
|
||||
<span class="robot-card-main">
|
||||
<span class="robot-card-title" data-i18n="settings.robots.discord.title">Discord</span>
|
||||
<span class="robot-card-desc" data-i18n="settings.robots.discord.subtitle">Gateway WebSocket,私聊与服务器 @</span>
|
||||
@@ -3598,7 +3598,7 @@
|
||||
<span class="robot-card-action" data-i18n="settings.robots.configure">配置</span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="robot-card" data-robot-card="qq" onclick="openRobotEditor('qq')">
|
||||
<button type="button" class="robot-card" data-robot-card="qq" data-require-permission="robot:write" onclick="openRobotEditor('qq')">
|
||||
<span class="robot-card-main">
|
||||
<span class="robot-card-title" data-i18n="settings.robots.qq.title">QQ 机器人</span>
|
||||
<span class="robot-card-desc" data-i18n="settings.robots.qq.subtitle">QQ 开放平台 WebSocket,C2C 与群 @</span>
|
||||
@@ -3634,7 +3634,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="robot-wechat-action-row">
|
||||
<button type="button" class="btn-primary" id="robot-wechat-bind-btn" onclick="startWechatRobotBind()" data-i18n="settings.robots.wechat.bindButton">生成二维码并绑定</button>
|
||||
<button type="button" class="btn-primary" data-require-permission="robot:write" id="robot-wechat-bind-btn" onclick="startWechatRobotBind()" data-i18n="settings.robots.wechat.bindButton">生成二维码并绑定</button>
|
||||
<p class="robot-wechat-hint" id="robot-wechat-bind-hint" data-i18n="settings.robots.wechat.bindHint">用微信扫码确认后会自动保存并启用。</p>
|
||||
</div>
|
||||
<div id="robot-wechat-bound-flash" class="robot-wechat-bound-flash" hidden role="status">
|
||||
@@ -3663,7 +3663,7 @@
|
||||
<label for="robot-wechat-verify-code" data-i18n="settings.robots.wechat.verifyCodeLabel">手机显示的数字(仅部分账号需要)</label>
|
||||
<div class="robot-wechat-verify-row">
|
||||
<input type="text" id="robot-wechat-verify-code" inputmode="numeric" autocomplete="one-time-code" maxlength="8" placeholder="000000" />
|
||||
<button type="button" class="btn-primary btn-sm" onclick="submitWechatVerifyCode()" data-i18n="settings.robots.wechat.verifyCodeSubmit">提交</button>
|
||||
<button type="button" class="btn-primary btn-sm" data-require-permission="robot:write" onclick="submitWechatVerifyCode()" data-i18n="settings.robots.wechat.verifyCodeSubmit">提交</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3982,7 +3982,7 @@
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
|
||||
<button class="btn-primary" onclick="applySettings()" data-require-permission="config:write" data-i18n="settings.apply.button">应用配置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4101,8 +4101,8 @@
|
||||
<span class="audit-export-caret" aria-hidden="true">▾</span>
|
||||
</button>
|
||||
<div id="audit-export-menu" class="audit-export-menu" role="menu" hidden>
|
||||
<button type="button" class="audit-export-menu-item" role="menuitem" onclick="runAuditExport('json')" data-i18n="settingsAudit.exportJson">导出 JSON</button>
|
||||
<button type="button" class="audit-export-menu-item" role="menuitem" onclick="runAuditExport('csv')" data-i18n="settingsAudit.exportCsv">导出 CSV</button>
|
||||
<button type="button" class="audit-export-menu-item" role="menuitem" data-require-permission="audit:read" onclick="runAuditExport('json')" data-i18n="settingsAudit.exportJson">导出 JSON</button>
|
||||
<button type="button" class="audit-export-menu-item" role="menuitem" data-require-permission="audit:read" onclick="runAuditExport('csv')" data-i18n="settingsAudit.exportCsv">导出 CSV</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4137,7 +4137,7 @@
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn-secondary" type="button" onclick="resetPasswordForm()" data-i18n="settingsSecurity.clear">清空</button>
|
||||
<button class="btn-primary change-password-submit" type="button" onclick="changePassword()" data-i18n="settingsSecurity.changePasswordBtn">修改密码</button>
|
||||
<button class="btn-primary change-password-submit" type="button" data-require-permission="auth:self" onclick="changePassword()" data-i18n="settingsSecurity.changePasswordBtn">修改密码</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4214,7 +4214,7 @@
|
||||
<p id="rbac-selected-subtitle" class="rbac-muted" data-i18n="rbac.selectUserHint">选择一个平台用户后编辑角色、状态与资源授权。</p>
|
||||
</div>
|
||||
<div class="rbac-detail-actions">
|
||||
<button class="btn-secondary btn-small btn-delete" type="button" id="rbac-delete-user-btn" onclick="deleteSelectedRbacUser()" disabled data-i18n="common.delete">删除</button>
|
||||
<button class="btn-secondary btn-small btn-delete" data-require-permission="rbac:write" type="button" id="rbac-delete-user-btn" onclick="deleteSelectedRbacUser()" disabled data-i18n="common.delete">删除</button>
|
||||
<button class="btn-primary btn-small" type="button" id="rbac-edit-user-btn" onclick="openRbacUserModalForSelected()" disabled data-i18n="common.edit">编辑</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4235,7 +4235,7 @@
|
||||
<div class="rbac-role-save-actions">
|
||||
<span id="rbac-role-unsaved" class="rbac-unsaved-indicator" hidden data-i18n="rbac.unsavedChanges">有未保存的变更</span>
|
||||
<button id="rbac-role-reset-btn" class="btn-secondary btn-small" type="button" onclick="resetSelectedUserRoles()" disabled data-i18n="common.cancel">取消</button>
|
||||
<button id="rbac-role-apply-btn" class="btn-primary btn-small" type="button" onclick="saveSelectedUserRoles()" disabled data-i18n="rbac.saveRoleChanges">保存角色变更</button>
|
||||
<button id="rbac-role-apply-btn" class="btn-primary btn-small" data-require-permission="rbac:write" type="button" onclick="saveSelectedUserRoles()" disabled data-i18n="rbac.saveRoleChanges">保存角色变更</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="rbac-roles-list" class="rbac-role-grid"></div>
|
||||
@@ -4278,7 +4278,7 @@
|
||||
<div id="rbac-pending-list" class="rbac-pending-list"></div>
|
||||
<div class="rbac-pending-bar-actions">
|
||||
<button type="button" class="btn-secondary btn-small" onclick="clearRbacResourceSelection()" data-i18n="rbac.clearSelection">清空选择</button>
|
||||
<button type="button" class="btn-primary btn-small" onclick="createRbacAssignment()" data-i18n="rbac.confirmGrant">确认授权</button>
|
||||
<button type="button" class="btn-primary btn-small" data-require-permission="rbac:write" onclick="createRbacAssignment()" data-i18n="rbac.confirmGrant">确认授权</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rbac-assignment-columns">
|
||||
@@ -4529,7 +4529,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeExternalMCPModal()" data-i18n="common.cancel">取消</button>
|
||||
<button class="btn-primary" onclick="saveExternalMCP()" data-i18n="common.save">保存</button>
|
||||
<button class="btn-primary" data-require-permission="mcp:write" onclick="saveExternalMCP()" data-i18n="common.save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4540,7 +4540,7 @@
|
||||
<div class="modal-header">
|
||||
<h2 data-i18n="attackChainModal.title">攻击链可视化</h2>
|
||||
<div class="modal-header-actions">
|
||||
<button class="btn-primary attack-chain-action-btn" onclick="regenerateAttackChain()" data-i18n="attackChainModal.regenerateTitle" data-i18n-attr="title" data-i18n-skip-text="true" title="重新生成攻击链(包含最新对话内容)">
|
||||
<button class="btn-primary attack-chain-action-btn" data-require-permission="attackchain:write" onclick="regenerateAttackChain()" data-i18n="attackChainModal.regenerateTitle" data-i18n-attr="title" data-i18n-skip-text="true" title="重新生成攻击链(包含最新对话内容)">
|
||||
<span data-i18n="attackChainModal.regenerate">重新生成</span>
|
||||
</button>
|
||||
<button class="btn-secondary attack-chain-action-btn" onclick="exportAttackChain('png')" data-i18n="attackChainModal.exportPng" data-i18n-attr="title" title="导出为PNG">
|
||||
@@ -4657,7 +4657,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-secondary" onclick="closeChatFilesEditModal()" data-i18n="common.cancel">取消</button>
|
||||
<button type="button" class="btn-primary" onclick="saveChatFilesEdit()" data-i18n="common.save">保存</button>
|
||||
<button type="button" class="btn-primary" data-require-permission="files:write" onclick="saveChatFilesEdit()" data-i18n="common.save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4818,7 +4818,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeSkillModal()" data-i18n="common.cancel">取消</button>
|
||||
<button class="btn-primary" onclick="saveSkill()" data-i18n="common.save">保存</button>
|
||||
<button class="btn-primary" data-require-permission="skills:write" onclick="saveSkill()" data-i18n="common.save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4845,7 +4845,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeKnowledgeItemModal()">取消</button>
|
||||
<button class="btn-primary" onclick="saveKnowledgeItem()">保存</button>
|
||||
<button class="btn-primary" data-require-permission="knowledge:write" onclick="saveKnowledgeItem()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4898,7 +4898,7 @@
|
||||
</div>
|
||||
<div class="batch-footer-actions">
|
||||
<button class="btn-secondary" onclick="closeBatchManageModal()" data-i18n="common.cancel">取消</button>
|
||||
<button class="btn-primary" onclick="deleteSelectedConversations()" data-i18n="batchManageModal.deleteSelected">删除所选</button>
|
||||
<button class="btn-primary" data-require-permission="chat:delete" onclick="deleteSelectedConversations()" data-i18n="batchManageModal.deleteSelected">删除所选</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4962,7 +4962,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeCreateGroupModal()" data-i18n="createGroupModal.cancel">取消</button>
|
||||
<button class="btn-primary" onclick="createGroup(event)" data-i18n="createGroupModal.create">创建</button>
|
||||
<button class="btn-primary" data-require-permission="group:write" onclick="createGroup(event)" data-i18n="createGroupModal.create">创建</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5036,7 +5036,7 @@
|
||||
<div id="move-to-group-submenu" class="context-submenu" style="display: none;" onmouseenter="clearSubmenuHideTimeout()" onmouseleave="hideMoveToGroupSubmenu()"></div>
|
||||
</div>
|
||||
<div class="context-menu-divider"></div>
|
||||
<div class="context-menu-item context-menu-item-danger" onclick="deleteConversationFromContext()">
|
||||
<div class="context-menu-item context-menu-item-danger" data-require-permission="chat:delete" onclick="deleteConversationFromContext()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
@@ -5059,7 +5059,7 @@
|
||||
</svg>
|
||||
<span id="pin-group-menu-text" data-i18n="contextMenu.pinGroup">置顶此分组</span>
|
||||
</div>
|
||||
<div class="context-menu-item context-menu-item-danger" onclick="deleteGroupFromContext()">
|
||||
<div class="context-menu-item context-menu-item-danger" data-require-permission="group:delete" onclick="deleteGroupFromContext()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
@@ -5069,14 +5069,14 @@
|
||||
|
||||
<!-- 项目列表操作菜单 -->
|
||||
<div id="projects-list-action-menu" class="context-menu" style="display: none;" role="menu">
|
||||
<div id="projects-list-menu-edit" class="context-menu-item" onclick="editProjectFromListMenu()">
|
||||
<div id="projects-list-menu-edit" class="context-menu-item" data-require-permission="project:write" onclick="editProjectFromListMenu()">
|
||||
<span id="projects-list-menu-edit-text"></span>
|
||||
</div>
|
||||
<div id="projects-list-menu-archive" class="context-menu-item" onclick="toggleProjectArchiveFromListMenu()">
|
||||
<div id="projects-list-menu-archive" class="context-menu-item" data-require-permission="project:write" onclick="toggleProjectArchiveFromListMenu()">
|
||||
<span id="projects-list-menu-archive-text"></span>
|
||||
</div>
|
||||
<div class="context-menu-divider"></div>
|
||||
<div class="context-menu-item context-menu-item-danger" onclick="deleteProjectFromListMenu()">
|
||||
<div class="context-menu-item context-menu-item-danger" data-require-permission="project:delete" onclick="deleteProjectFromListMenu()">
|
||||
<span id="projects-list-menu-delete-text"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5154,7 +5154,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeBatchImportModal()" data-i18n="common.cancel">取消</button>
|
||||
<button class="btn-primary" onclick="createBatchQueue()" data-i18n="batchImportModal.createQueue">创建队列</button>
|
||||
<button class="btn-primary" data-require-permission="tasks:write" onclick="createBatchQueue()" data-i18n="batchImportModal.createQueue">创建队列</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5166,11 +5166,11 @@
|
||||
<h2 id="batch-queue-detail-title" data-i18n="batchQueueDetailModal.title">批量任务队列详情</h2>
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<div class="modal-header-actions">
|
||||
<button class="btn-secondary btn-small" id="batch-queue-add-task-btn" onclick="showAddBatchTaskModal()" style="display: none;" data-i18n="batchQueueDetailModal.addTask">添加任务</button>
|
||||
<button class="btn-primary btn-small" id="batch-queue-start-btn" onclick="startBatchQueue()" style="display: none;" data-i18n="batchQueueDetailModal.startExecute">开始执行</button>
|
||||
<button class="btn-primary btn-small" id="batch-queue-rerun-btn" onclick="rerunBatchQueue()" style="display: none;" data-i18n="batchQueueDetailModal.rerunQueue">重跑一轮</button>
|
||||
<button class="btn-secondary btn-small" id="batch-queue-pause-btn" onclick="pauseBatchQueue()" style="display: none;" data-i18n="batchQueueDetailModal.pauseQueue">暂停队列</button>
|
||||
<button class="btn-secondary btn-small btn-danger" id="batch-queue-delete-btn" onclick="deleteBatchQueue()" style="display: none;" data-i18n="batchQueueDetailModal.deleteQueue">删除队列</button>
|
||||
<button class="btn-secondary btn-small" data-require-permission="tasks:write" id="batch-queue-add-task-btn" onclick="showAddBatchTaskModal()" style="display: none;" data-i18n="batchQueueDetailModal.addTask">添加任务</button>
|
||||
<button class="btn-primary btn-small" data-require-permission="tasks:write" id="batch-queue-start-btn" onclick="startBatchQueue()" style="display: none;" data-i18n="batchQueueDetailModal.startExecute">开始执行</button>
|
||||
<button class="btn-primary btn-small" data-require-permission="tasks:write" id="batch-queue-rerun-btn" onclick="rerunBatchQueue()" style="display: none;" data-i18n="batchQueueDetailModal.rerunQueue">重跑一轮</button>
|
||||
<button class="btn-secondary btn-small" data-require-permission="tasks:write" id="batch-queue-pause-btn" onclick="pauseBatchQueue()" style="display: none;" data-i18n="batchQueueDetailModal.pauseQueue">暂停队列</button>
|
||||
<button class="btn-secondary btn-small btn-danger" data-require-permission="tasks:delete" id="batch-queue-delete-btn" onclick="deleteBatchQueue()" style="display: none;" data-i18n="batchQueueDetailModal.deleteQueue">删除队列</button>
|
||||
</div>
|
||||
<span class="modal-close" onclick="closeBatchQueueDetailModal()">×</span>
|
||||
</div>
|
||||
@@ -5194,7 +5194,7 @@
|
||||
<textarea id="add-task-message" class="form-control" rows="5" data-i18n="addBatchTaskModal.taskMessagePlaceholder" data-i18n-attr="placeholder" placeholder="请输入任务消息"></textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn-primary" onclick="saveAddBatchTask()" data-i18n="addBatchTaskModal.add">添加</button>
|
||||
<button class="btn-primary" data-require-permission="tasks:write" onclick="saveAddBatchTask()" data-i18n="addBatchTaskModal.add">添加</button>
|
||||
<button class="btn-secondary" onclick="closeAddBatchTaskModal()" data-i18n="common.cancel">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5292,7 +5292,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeVulnerabilityModal()" data-i18n="common.cancel">取消</button>
|
||||
<button class="btn-primary" onclick="saveVulnerability()" data-i18n="common.save">保存</button>
|
||||
<button class="btn-primary" data-require-permission="vulnerability:write" onclick="saveVulnerability()" data-i18n="common.save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5362,7 +5362,7 @@
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-secondary" id="webshell-test-btn" onclick="testWebshellConnection()" data-i18n="webshell.testConnectivity">测试连通性</button>
|
||||
<button class="btn-secondary" onclick="closeWebshellModal()" data-i18n="common.cancel">取消</button>
|
||||
<button class="btn-primary" onclick="saveWebshellConnection()" data-i18n="common.save">保存</button>
|
||||
<button class="btn-primary" data-require-permission="webshell:write" onclick="saveWebshellConnection()" data-i18n="common.save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5470,7 +5470,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeRoleModal()" data-i18n="common.cancel">取消</button>
|
||||
<button class="btn-primary" onclick="saveRole()" data-i18n="common.save">保存</button>
|
||||
<button class="btn-primary" data-require-permission="roles:write" onclick="saveRole()" data-i18n="common.save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5515,7 +5515,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" type="button" onclick="closeRbacUserModal()" data-i18n="common.cancel">取消</button>
|
||||
<button class="btn-primary" type="button" onclick="saveRbacUser()" data-i18n="common.save">保存</button>
|
||||
<button class="btn-primary" data-require-permission="rbac:write" type="button" onclick="saveRbacUser()" data-i18n="common.save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5557,7 +5557,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" type="button" onclick="closeRbacRoleModal()" data-i18n="common.cancel">取消</button>
|
||||
<button class="btn-primary" id="rbac-role-save-btn" type="button" onclick="saveRbacRole()" data-i18n="common.save">保存</button>
|
||||
<button class="btn-primary" data-require-permission="rbac:write" id="rbac-role-save-btn" type="button" onclick="saveRbacRole()" data-i18n="common.save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5587,7 +5587,7 @@
|
||||
</div>
|
||||
<div class="projects-modal-footer">
|
||||
<button class="btn-secondary" type="button" onclick="closeProjectModal()" data-i18n="common.cancel">取消</button>
|
||||
<button class="btn-primary" type="button" id="project-modal-submit-btn" onclick="saveProjectModal()" data-i18n="projects.createProject">创建项目</button>
|
||||
<button class="btn-primary" data-require-permission="project:write" type="button" id="project-modal-submit-btn" onclick="saveProjectModal()" data-i18n="projects.createProject">创建项目</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5664,7 +5664,7 @@
|
||||
</div>
|
||||
<div class="projects-modal-footer">
|
||||
<button class="btn-secondary" type="button" onclick="closeFactModal()" data-i18n="common.cancel">取消</button>
|
||||
<button class="btn-primary" type="button" id="fact-modal-submit-btn" onclick="saveFactModal()" data-i18n="projects.saveFact">保存事实</button>
|
||||
<button class="btn-primary" data-require-permission="project:write" type="button" id="fact-modal-submit-btn" onclick="saveFactModal()" data-i18n="projects.saveFact">保存事实</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5686,7 +5686,7 @@
|
||||
<div class="projects-modal-footer projects-modal-footer--split">
|
||||
<div class="projects-modal-footer-left">
|
||||
<button class="btn-secondary btn-small" type="button" id="fact-detail-link-vuln-btn" onclick="linkFactToExistingVulnerability()" hidden data-i18n="projects.linkVulnerability">关联漏洞</button>
|
||||
<button class="btn-secondary btn-small" type="button" id="fact-detail-create-vuln-btn" onclick="createVulnerabilityFromCurrentFact()" hidden data-i18n="projects.createVulnerabilityDraft">生成漏洞草稿</button>
|
||||
<button class="btn-secondary btn-small" data-require-permission="vulnerability:write" type="button" id="fact-detail-create-vuln-btn" onclick="createVulnerabilityFromCurrentFact()" hidden data-i18n="projects.createVulnerabilityDraft">生成漏洞草稿</button>
|
||||
</div>
|
||||
<div class="projects-modal-footer-right">
|
||||
<button class="btn-secondary" type="button" onclick="closeFactDetailModal()" data-i18n="common.close">关闭</button>
|
||||
@@ -5729,5 +5729,6 @@
|
||||
<script src="/static/js/roles.js"></script>
|
||||
<script src="/static/js/rbac.js"></script>
|
||||
<script src="/static/js/c2.js"></script>
|
||||
<script src="/static/js/rbac-guards.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user