Add files via upload

This commit is contained in:
公明
2026-06-14 19:42:14 +08:00
committed by GitHub
parent 54cea1b172
commit 6c4b3bf131
5 changed files with 311 additions and 32 deletions
+90 -3
View File
@@ -22514,7 +22514,10 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
}
.projects-list-item {
position: relative;
padding: 10px 12px 10px 14px;
display: flex;
align-items: center;
gap: 4px;
padding: 10px 8px 10px 14px;
border-radius: 8px;
cursor: pointer;
font-size: 0.875rem;
@@ -22547,8 +22550,43 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
color: #94a3b8;
}
.projects-list-item-body {
flex: 1;
min-width: 0;
}
.projects-list-item-menu {
width: 24px;
height: 24px;
padding: 0;
border: none;
background: transparent;
color: var(--text-muted, #94a3b8);
cursor: pointer;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 16px;
font-weight: 600;
line-height: 1;
opacity: 0;
transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease;
}
.projects-list-item:hover .projects-list-item-menu,
.projects-list-item.is-active .projects-list-item-menu {
opacity: 0.75;
}
.projects-list-item-menu:hover,
.projects-list-item-menu:focus-visible {
opacity: 1;
background: #e2e8f0;
color: var(--text-primary, #0f172a);
outline: none;
}
.projects-list-item.is-active .projects-list-item-menu:hover,
.projects-list-item.is-active .projects-list-item-menu:focus-visible {
background: #dbeafe;
}
.projects-list-item-name {
font-weight: 600;
color: var(--text-primary, #0f172a);
@@ -22679,12 +22717,61 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
font-size: 0.8125rem;
color: #94a3b8;
}
.projects-detail-desc {
.projects-detail-desc-block {
margin: 10px 0 0;
max-width: min(640px, 100%);
}
.projects-detail-desc {
margin: 0;
font-size: 0.875rem;
color: #475569;
line-height: 1.55;
max-width: 640px;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
}
.projects-detail-desc.is-collapsed {
max-height: 4.65em;
overflow: hidden;
position: relative;
}
.projects-detail-desc.is-collapsed::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 1.4em;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), #fff 85%);
pointer-events: none;
}
.projects-detail-desc.is-expanded {
max-height: min(240px, 32vh);
overflow-y: auto;
overscroll-behavior: contain;
padding-right: 4px;
}
.projects-detail-desc-toggle {
display: inline-block;
margin-top: 6px;
padding: 0;
border: none;
background: none;
color: #0066ff;
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
line-height: 1.4;
}
.projects-detail-desc-toggle:hover,
.projects-detail-desc-toggle:focus-visible {
text-decoration: underline;
outline: none;
}
.projects-description-textarea {
max-height: 200px;
resize: vertical;
overflow-y: auto;
}
.projects-detail-stats {
display: flex;
+6
View File
@@ -324,6 +324,9 @@
"statsSparse": "{{count}} incomplete",
"projectNotFound": "Project not found",
"updatedPrefix": "Updated {{time}}",
"descExpand": "Show all",
"descCollapse": "Show less",
"descriptionLengthHint": "Keep it brief (max 4000 chars). Put long logs/POCs in fact board body instead.",
"noMatchingFacts": "No matching facts, try adjusting filters",
"noFacts": "No facts yet. Click Add fact or let Agent write facts automatically",
"relatedVulnIdTitle": "Related vulnerability ID",
@@ -407,6 +410,9 @@
"dangerZoneTitle": "Danger zone",
"dangerZoneHint": "Archived projects are hidden unless 'Show archived' is enabled; deletion removes all facts permanently.",
"archiveRestore": "Archive / Restore",
"archiveProject": "Archive",
"restoreProjectActive": "Restore to active",
"projectActions": "Project actions",
"deleteProject": "Delete project",
"saveChangesHint": "Click save to sync changes to server",
"saveSettings": "Save changes",
+6
View File
@@ -312,6 +312,9 @@
"statsSparse": "{{count}} 待补全",
"projectNotFound": "项目不存在",
"updatedPrefix": "更新于 {{time}}",
"descExpand": "展开全部",
"descCollapse": "收起",
"descriptionLengthHint": "简要说明即可(最多 4000 字);大段日志/POC 请写入事实黑板 body",
"noMatchingFacts": "无匹配事实,请调整筛选条件",
"noFacts": "暂无事实,点击「添加事实」或由 Agent 自动写入",
"relatedVulnIdTitle": "关联漏洞 ID",
@@ -395,6 +398,9 @@
"dangerZoneTitle": "危险操作",
"dangerZoneHint": "归档后需在列表勾选「显示已归档」才能查看;删除将清除全部事实且不可恢复。",
"archiveRestore": "归档 / 恢复",
"archiveProject": "归档",
"restoreProjectActive": "恢复为进行中",
"projectActions": "项目操作",
"deleteProject": "删除项目",
"saveChangesHint": "修改后请点击保存以同步到服务器",
"saveSettings": "保存更改",
+190 -26
View File
@@ -11,6 +11,8 @@ let _projectsListReady = false;
let _projectsFetchPromise = null;
const PROJECT_ACTIVE_KEY = 'cyberstrike.activeProjectId';
const PROJECT_DESCRIPTION_MAX_LENGTH = 4000;
const PROJECT_DESC_COLLAPSE_THRESHOLD = 180;
function tp(key, opts) {
if (typeof window.t === 'function') return window.t(key, opts);
@@ -611,11 +613,71 @@ function renderProjectsSidebar() {
<div class="projects-list-item-name">${escapeHtml(p.name)}${badges}</div>
<div class="projects-list-item-meta">${formatProjectTime(p.updated_at)}</div>
</div>
<button type="button" class="projects-list-item-menu" title="${escapeHtml(tp('projects.projectActions'))}" aria-label="${escapeHtml(tp('projects.projectActions'))}" onclick="showProjectListActionMenu(event, '${escapeHtml(p.id)}')">⋯</button>
</div>`;
}).join('');
updateProjectsDetailVisibility();
}
function clampProjectDescription(text) {
const s = (text || '').trim();
if (s.length <= PROJECT_DESCRIPTION_MAX_LENGTH) return s;
return s.slice(0, PROJECT_DESCRIPTION_MAX_LENGTH);
}
function projectDescriptionNeedsToggle(text) {
const s = (text || '').trim();
if (!s) return false;
if (s.length > PROJECT_DESC_COLLAPSE_THRESHOLD) return true;
return s.split('\n').length > 3;
}
function renderProjectDetailDesc(desc) {
const blockEl = document.getElementById('projects-detail-desc-block');
const descEl = document.getElementById('projects-detail-desc');
const toggleEl = document.getElementById('projects-detail-desc-toggle');
if (!descEl || !blockEl) return;
const text = (desc || '').trim();
if (!text) {
blockEl.hidden = true;
descEl.textContent = '';
descEl.className = 'projects-detail-desc is-collapsed';
if (toggleEl) {
toggleEl.hidden = true;
toggleEl.dataset.expanded = 'false';
}
return;
}
descEl.textContent = text;
blockEl.hidden = false;
descEl.classList.remove('is-expanded');
descEl.classList.add('is-collapsed');
if (toggleEl) {
const needsToggle = projectDescriptionNeedsToggle(text);
toggleEl.hidden = !needsToggle;
toggleEl.textContent = tp('projects.descExpand');
toggleEl.dataset.expanded = 'false';
}
}
function toggleProjectDetailDesc() {
const descEl = document.getElementById('projects-detail-desc');
const toggleEl = document.getElementById('projects-detail-desc-toggle');
if (!descEl || !toggleEl || toggleEl.hidden) return;
const expanded = toggleEl.dataset.expanded === 'true';
if (expanded) {
descEl.classList.add('is-collapsed');
descEl.classList.remove('is-expanded');
toggleEl.textContent = tp('projects.descExpand');
toggleEl.dataset.expanded = 'false';
} else {
descEl.classList.remove('is-collapsed');
descEl.classList.add('is-expanded');
toggleEl.textContent = tp('projects.descCollapse');
toggleEl.dataset.expanded = 'true';
}
}
function updateProjectStatusPill(status) {
const el = document.getElementById('projects-detail-status');
if (!el) return;
@@ -682,16 +744,7 @@ async function selectProject(id) {
const metaEl = document.getElementById('projects-detail-meta');
if (metaEl) metaEl.textContent = tpFmt('projects.updatedPrefix', `Updated ${formatProjectTime(p.updated_at)}`, { time: formatProjectTime(p.updated_at) });
const descEl = document.getElementById('projects-detail-desc');
if (descEl) {
const desc = (p.description || '').trim();
if (desc) {
descEl.textContent = desc;
descEl.hidden = false;
} else {
descEl.textContent = '';
descEl.hidden = true;
}
}
if (descEl) renderProjectDetailDesc(p.description);
projectNameById[p.id] = p.name || p.id;
} catch (e) {
console.warn(e);
@@ -1205,7 +1258,7 @@ async function saveProjectModal() {
if (!name) return alert(tp('projects.enterProjectName'));
const body = {
name,
description: document.getElementById('project-modal-description').value.trim(),
description: clampProjectDescription(document.getElementById('project-modal-description').value),
};
const editId = window._projectModalEditId;
const res = editId
@@ -1272,7 +1325,7 @@ async function saveProjectSettings() {
}
const body = {
name: document.getElementById('project-edit-name').value.trim(),
description: document.getElementById('project-edit-description').value.trim(),
description: clampProjectDescription(document.getElementById('project-edit-description').value),
scope_json: scopeRaw,
status: document.getElementById('project-edit-status')?.value || 'active',
pinned: !!document.getElementById('project-edit-pinned')?.checked,
@@ -1288,30 +1341,110 @@ async function saveProjectSettings() {
alert(tp('projects.saved'));
}
async function archiveCurrentProject() {
if (!currentProjectId) return;
const statusEl = document.getElementById('project-edit-status');
const cur = statusEl?.value || 'active';
function findProjectById(projectId) {
return projectsCache.find((p) => p.id === projectId) || projectsCacheAll.find((p) => p.id === projectId);
}
let _projectListMenuTargetId = null;
let _projectListMenuDocClickBound = false;
function closeProjectListActionMenu() {
const menu = document.getElementById('projects-list-action-menu');
if (!menu) return;
menu.style.display = 'none';
_projectListMenuTargetId = null;
}
function positionProjectListActionMenu(event) {
const menu = document.getElementById('projects-list-action-menu');
if (!menu) return;
menu.style.display = 'block';
menu.style.visibility = 'visible';
menu.style.opacity = '1';
void menu.offsetHeight;
const menuRect = menu.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let left = event.clientX;
let top = event.clientY;
if (left + menuRect.width > viewportWidth) {
left = Math.max(8, event.clientX - menuRect.width);
}
if (top + menuRect.height > viewportHeight) {
top = Math.max(8, event.clientY - menuRect.height);
}
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
}
function showProjectListActionMenu(event, projectId) {
event.stopPropagation();
event.preventDefault();
const menu = document.getElementById('projects-list-action-menu');
if (!menu) return;
if (_projectListMenuTargetId === projectId && menu.style.display === 'block') {
closeProjectListActionMenu();
return;
}
closeProjectListActionMenu();
const p = findProjectById(projectId);
if (!p) return;
_projectListMenuTargetId = projectId;
const archiveText = document.getElementById('projects-list-menu-archive-text');
const deleteText = document.getElementById('projects-list-menu-delete-text');
if (archiveText) {
archiveText.textContent = p.status === 'archived'
? tp('projects.restoreProjectActive')
: tp('projects.archiveProject');
}
if (deleteText) deleteText.textContent = tp('projects.deleteProject');
positionProjectListActionMenu(event);
}
function initProjectListActionMenu() {
if (_projectListMenuDocClickBound) return;
_projectListMenuDocClickBound = true;
document.addEventListener('click', (event) => {
const menu = document.getElementById('projects-list-action-menu');
if (!menu || menu.style.display === 'none') return;
if (menu.contains(event.target)) return;
if (event.target.closest('.projects-list-item-menu')) return;
closeProjectListActionMenu();
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') closeProjectListActionMenu();
});
}
async function toggleProjectArchiveById(projectId) {
const p = findProjectById(projectId);
if (!p) return;
const cur = p.status || 'active';
const next = cur === 'archived' ? 'active' : 'archived';
if (!confirm(next === 'archived' ? tp('projects.confirmArchiveProject') : tp('projects.confirmRestoreProjectActive'))) return;
const res = await apiFetch(`/api/projects/${currentProjectId}`, {
const res = await apiFetch(`/api/projects/${projectId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: next }),
});
if (!res.ok) return alert(tp('projects.operationFailed'));
await loadProjectsList();
await selectProject(currentProjectId);
if (currentProjectId === projectId && projectsCache.some((item) => item.id === projectId)) {
await selectProject(projectId);
} else if (currentProjectId === projectId) {
currentProjectId = null;
updateProjectsDetailVisibility();
if (projectsCache.length) await selectProject(projectsCache[0].id);
}
}
async function deleteCurrentProject() {
if (!currentProjectId || !confirm(tp('projects.confirmDeleteProject'))) return;
const deletedId = currentProjectId;
const deletedIndex = projectsCache.findIndex((p) => p.id === deletedId);
const res = await apiFetch(`/api/projects/${deletedId}`, { method: 'DELETE' });
async function deleteProjectById(projectId) {
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 (getActiveProjectId() === deletedId) setActiveProjectId('');
currentProjectId = null;
if (getActiveProjectId() === projectId) setActiveProjectId('');
if (currentProjectId === projectId) currentProjectId = null;
await loadProjectsList();
if (projectsCache.length) {
const nextIndex = Math.min(deletedIndex >= 0 ? deletedIndex : 0, projectsCache.length - 1);
@@ -1321,6 +1454,30 @@ async function deleteCurrentProject() {
}
}
async function toggleProjectArchiveFromListMenu() {
const projectId = _projectListMenuTargetId;
closeProjectListActionMenu();
if (!projectId) return;
await toggleProjectArchiveById(projectId);
}
async function deleteProjectFromListMenu() {
const projectId = _projectListMenuTargetId;
closeProjectListActionMenu();
if (!projectId) return;
await deleteProjectById(projectId);
}
async function archiveCurrentProject() {
if (!currentProjectId) return;
await toggleProjectArchiveById(currentProjectId);
}
async function deleteCurrentProject() {
if (!currentProjectId) return;
await deleteProjectById(currentProjectId);
}
function resetFactModalForm() {
window._factModalEditId = null;
const keyEl = document.getElementById('fact-modal-key');
@@ -1731,9 +1888,13 @@ function initChatProjectSelector() {
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initChatProjectSelector);
document.addEventListener('DOMContentLoaded', () => {
initChatProjectSelector();
initProjectListActionMenu();
});
} else {
initChatProjectSelector();
initProjectListActionMenu();
}
window.initProjectsPage = initProjectsPage;
@@ -1752,6 +1913,9 @@ window.closeFactDetailModal = closeFactDetailModal;
window.saveProjectSettings = saveProjectSettings;
window.archiveCurrentProject = archiveCurrentProject;
window.deleteCurrentProject = deleteCurrentProject;
window.showProjectListActionMenu = showProjectListActionMenu;
window.toggleProjectArchiveFromListMenu = toggleProjectArchiveFromListMenu;
window.deleteProjectFromListMenu = deleteProjectFromListMenu;
window.refreshChatProjectSelector = refreshChatProjectSelector;
window.onChatProjectChange = onChatProjectChange;
window.toggleChatProjectPanel = toggleChatProjectPanel;
+19 -3
View File
@@ -1476,7 +1476,10 @@
<span id="projects-detail-status" class="projects-status-pill projects-status-pill--active" data-i18n="projects.statusActive">进行中</span>
</div>
<p id="projects-detail-meta" class="projects-detail-meta"></p>
<p id="projects-detail-desc" class="projects-detail-desc"></p>
<div id="projects-detail-desc-block" class="projects-detail-desc-block" hidden>
<p id="projects-detail-desc" class="projects-detail-desc is-collapsed"></p>
<button type="button" id="projects-detail-desc-toggle" class="projects-detail-desc-toggle" hidden onclick="toggleProjectDetailDesc()"></button>
</div>
<div class="projects-detail-stats" id="projects-detail-stats">
<span class="projects-stat-chip" id="project-stat-facts">0 条事实</span>
<span class="projects-stat-chip" id="project-stat-vulns">0 个漏洞</span>
@@ -1669,7 +1672,8 @@
</div>
<div class="projects-form-field">
<label for="project-edit-description" data-i18n="projects.projectDescription">描述</label>
<textarea id="project-edit-description" class="form-input" rows="3" placeholder="测试目标、授权范围、联系人、注意事项…" data-i18n="projects.editDescriptionPlaceholder" data-i18n-attr="placeholder"></textarea>
<textarea id="project-edit-description" class="form-input projects-description-textarea" rows="3" maxlength="4000" placeholder="测试目标、授权范围、联系人、注意事项…" data-i18n="projects.editDescriptionPlaceholder" data-i18n-attr="placeholder"></textarea>
<small class="form-hint" data-i18n="projects.descriptionLengthHint">简要说明即可(最多 4000 字);大段日志/POC 请写入事实黑板 body</small>
</div>
</div>
</section>
@@ -3777,6 +3781,17 @@
</div>
</div>
<!-- 项目列表操作菜单 -->
<div id="projects-list-action-menu" class="context-menu" style="display: none;" role="menu">
<div id="projects-list-menu-archive" class="context-menu-item" 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()">
<span id="projects-list-menu-delete-text"></span>
</div>
</div>
<!-- 新建任务模态框 -->
<div id="batch-import-modal" class="modal">
<div class="modal-content" style="max-width: 800px;">
@@ -4159,7 +4174,8 @@
</div>
<div class="projects-form-field">
<label for="project-modal-description" data-i18n="projects.projectDescription">项目描述</label>
<textarea id="project-modal-description" class="form-input" rows="4" placeholder="测试范围、授权边界、注意事项…" data-i18n="projects.projectDescriptionPlaceholder" data-i18n-attr="placeholder"></textarea>
<textarea id="project-modal-description" class="form-input projects-description-textarea" rows="4" maxlength="4000" placeholder="测试范围、授权边界、注意事项…" data-i18n="projects.projectDescriptionPlaceholder" data-i18n-attr="placeholder"></textarea>
<small class="form-hint" data-i18n="projects.descriptionLengthHint">简要说明即可(最多 4000 字);大段日志/POC 请写入事实黑板 body</small>
</div>
</div>
<div class="projects-modal-footer">