From 2980d17dc7402fbd50dfbe13a7c27b792c5014d7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=85=AC=E6=98=8E?=
<83812544+Ed1s0nZ@users.noreply.github.com>
Date: Wed, 8 Jul 2026 14:39:30 +0800
Subject: [PATCH] Add files via upload
---
web/static/css/style.css | 84 +++++++-
web/static/i18n/en-US.json | 14 ++
web/static/i18n/zh-CN.json | 14 ++
web/static/js/chat.js | 411 ++++++++++++++++++++++++++++++++++++-
web/static/js/monitor.js | 62 ++++--
web/templates/index.html | 11 +
6 files changed, 558 insertions(+), 38 deletions(-)
diff --git a/web/static/css/style.css b/web/static/css/style.css
index 13df9f86..0cb32f9e 100644
--- a/web/static/css/style.css
+++ b/web/static/css/style.css
@@ -13841,8 +13841,8 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
/* 批量管理模态框 */
.batch-manage-modal-content {
- max-width: 920px;
- width: 90vw;
+ max-width: 1040px;
+ width: 92vw;
display: flex;
flex-direction: column;
max-height: 85vh;
@@ -13856,8 +13856,8 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
}
.batch-manage-header-actions .conversation-project-filter-ui {
- width: 148px;
- min-width: 108px;
+ width: 132px;
+ min-width: 100px;
flex-shrink: 0;
}
@@ -13911,7 +13911,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
.batch-table-header {
display: grid;
- grid-template-columns: 40px minmax(0, 1.2fr) minmax(0, 0.9fr) 160px 72px;
+ grid-template-columns: 40px minmax(0, 1.1fr) minmax(0, 0.75fr) minmax(0, 0.75fr) 160px 72px;
gap: 12px;
padding: 12px 16px;
background: var(--bg-secondary);
@@ -13930,7 +13930,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
.batch-conversation-row {
display: grid;
- grid-template-columns: 40px minmax(0, 1.2fr) minmax(0, 0.9fr) 160px 72px;
+ grid-template-columns: 40px minmax(0, 1.1fr) minmax(0, 0.75fr) minmax(0, 0.75fr) 160px 72px;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
@@ -13972,6 +13972,20 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
opacity: 0.85;
}
+.batch-table-col-group {
+ font-size: 0.8125rem;
+ color: var(--text-muted);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.batch-table-col-group.is-unbound {
+ color: var(--text-muted);
+ font-style: italic;
+ opacity: 0.85;
+}
+
.batch-table-col-time {
font-size: 0.875rem;
color: var(--text-muted);
@@ -14021,12 +14035,52 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
.batch-manage-footer {
display: flex;
align-items: center;
- justify-content: flex-end;
+ justify-content: space-between;
+ gap: 16px;
padding: 16px 24px;
border-top: 1px solid var(--border-color);
flex-shrink: 0;
}
+.batch-footer-move {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ min-width: 0;
+}
+
+.batch-footer-move .conversation-project-filter-ui {
+ width: 180px;
+ min-width: 140px;
+ flex-shrink: 0;
+}
+
+.batch-footer-move .conversation-project-filter-trigger {
+ font-size: 0.8125rem;
+ padding: 8px 10px;
+}
+
+.batch-manage-modal-content .batch-footer-move .conversation-project-filter-ui.open {
+ z-index: 400;
+}
+
+.batch-footer-move .conversation-project-filter-dropdown {
+ top: auto;
+ bottom: calc(100% + 4px);
+}
+
+.batch-move-group-select {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
.batch-table-col-checkbox input[type="checkbox"] {
cursor: pointer;
}
@@ -32038,6 +32092,22 @@ html[data-theme="dark"] .batch-search-box input::placeholder {
color: var(--text-muted) !important;
}
+html[data-theme="dark"] .batch-footer-move .conversation-project-filter-trigger,
+html[data-theme="dark"] .batch-footer-move .conversation-project-filter-dropdown {
+ background: #0f172a !important;
+ color: var(--text-primary) !important;
+ border-color: #263244 !important;
+}
+
+html[data-theme="dark"] .batch-footer-move .conversation-project-filter-option:hover {
+ background: #1e293b !important;
+}
+
+html[data-theme="dark"] .batch-footer-move .conversation-project-filter-option.is-selected {
+ background: rgba(59, 130, 246, 0.15) !important;
+ color: #93c5fd !important;
+}
+
html[data-theme="dark"] .batch-manage-modal-content,
html[data-theme="dark"] .batch-manage-modal-content .modal-header,
html[data-theme="dark"] .batch-manage-body,
diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json
index 1eb71442..b7d34ac9 100644
--- a/web/static/i18n/en-US.json
+++ b/web/static/i18n/en-US.json
@@ -2758,15 +2758,29 @@
"searchPlaceholder": "Search history",
"conversationName": "Conversation name",
"project": "Project",
+ "group": "Group",
"noProject": "No project",
"unknownProject": "Unknown project",
+ "noGroup": "Ungrouped",
+ "unknownGroup": "Unknown group",
"filterByProject": "Filter by project",
+ "filterByGroup": "Filter by group",
+ "filterAllGroups": "All groups",
+ "filterUngrouped": "Ungrouped",
"lastTime": "Last activity",
"action": "Action",
"selectAll": "Select all",
+ "setGroup": "Set group",
+ "noGroupOption": "Ungrouped",
"deleteSelected": "Delete selected",
"confirmDeleteNone": "Please select at least one conversation to delete",
"confirmDeleteN": "Delete {{count}} selected conversation(s)? Chat messages cannot be recovered, but recorded vulnerabilities will remain in the vulnerability library.",
+ "confirmGroupChangeNone": "Please select at least one conversation",
+ "confirmMoveN": "Move {{count}} selected conversation(s) to \"{{group}}\"?",
+ "confirmRemoveNoGroup": "None of the selected conversations belong to a group",
+ "confirmRemoveN": "Remove {{count}} selected conversation(s) from their group(s)?",
+ "removeFailed": "Remove failed",
+ "moveFailed": "Move failed",
"deleteFailed": "Delete failed",
"unnamedConversation": "Unnamed conversation"
},
diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json
index f2aaf649..346a32c8 100644
--- a/web/static/i18n/zh-CN.json
+++ b/web/static/i18n/zh-CN.json
@@ -2746,15 +2746,29 @@
"searchPlaceholder": "搜索历史记录",
"conversationName": "对话名称",
"project": "项目",
+ "group": "对话分组",
"noProject": "无项目",
"unknownProject": "未知项目",
+ "noGroup": "无分组",
+ "unknownGroup": "未知分组",
"filterByProject": "按项目筛选",
+ "filterByGroup": "按分组筛选",
+ "filterAllGroups": "全部分组",
+ "filterUngrouped": "无分组",
"lastTime": "最近一次对话时间",
"action": "操作",
"selectAll": "全选",
+ "setGroup": "设置分组",
+ "noGroupOption": "无分组",
"deleteSelected": "删除所选",
"confirmDeleteNone": "请先选择要删除的对话",
"confirmDeleteN": "确定要删除选中的 {{count}} 条对话吗?对话消息将不可恢复,但已记录的漏洞会保留在漏洞库中。",
+ "confirmGroupChangeNone": "请先选择要操作的对话",
+ "confirmMoveN": "确定将选中的 {{count}} 条对话移动到「{{group}}」吗?",
+ "confirmRemoveNoGroup": "所选对话均未归属分组",
+ "confirmRemoveN": "确定将选中的 {{count}} 条对话移出分组吗?",
+ "removeFailed": "移出失败",
+ "moveFailed": "移动失败",
"deleteFailed": "删除失败",
"unnamedConversation": "未命名对话"
},
diff --git a/web/static/js/chat.js b/web/static/js/chat.js
index 82fbc951..556036f0 100644
--- a/web/static/js/chat.js
+++ b/web/static/js/chat.js
@@ -2441,6 +2441,20 @@ function compactWorkflowProcessDetails(details) {
});
}
+function isProcessDetailsUserExpanded(messageId) {
+ const container = document.getElementById('process-details-' + messageId);
+ return !!(container && container.dataset && container.dataset.userExpanded === '1');
+}
+
+function syncProcessDetailButtonLabels(messageId, expanded) {
+ const expandT = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
+ const collapseT = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情';
+ const label = expanded ? collapseT : expandT;
+ document.querySelectorAll('#' + messageId + ' .process-detail-btn').forEach((btn) => {
+ btn.innerHTML = '' + label + '';
+ });
+}
+
// 渲染过程详情
// options.append=true 时分页追加;options.markLoaded=false 时保留 lazy 标记(分页加载中)
function renderProcessDetails(messageId, processDetails, options) {
@@ -2537,7 +2551,9 @@ function renderProcessDetails(messageId, processDetails, options) {
if (!processDetails || processDetails.length === 0) {
if (!appendMode) {
timeline.innerHTML = '
' + (typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)') + '
';
- timeline.classList.remove('expanded');
+ if (!isProcessDetailsUserExpanded(messageId)) {
+ timeline.classList.remove('expanded');
+ }
}
return;
}
@@ -2732,12 +2748,13 @@ function finishProcessDetailsRender(messageElement, processDetails, isLazyNotLoa
const hasErrorOrCancelled = processDetails.some(d =>
d.eventType === 'error' || d.eventType === 'cancelled'
);
- if (hasErrorOrCancelled && !hasPendingHitlInDetails && !hasPendingWorkflowHitl) {
+ const userExpanded = isProcessDetailsUserExpanded(messageElement.id);
+ if (userExpanded) {
+ timeline.classList.add('expanded');
+ syncProcessDetailButtonLabels(messageElement.id, true);
+ } else if (hasErrorOrCancelled && !hasPendingHitlInDetails && !hasPendingWorkflowHitl) {
timeline.classList.remove('expanded');
- const processDetailBtn = messageElement.querySelector('.process-detail-btn');
- if (processDetailBtn) {
- processDetailBtn.innerHTML = '' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '';
- }
+ syncProcessDetailButtonLabels(messageElement.id, false);
}
if (hasPendingWorkflowHitl && messageElement && messageElement.id) {
const convId = typeof window.currentConversationId === 'string' ? window.currentConversationId : '';
@@ -2983,6 +3000,8 @@ function toggleMcpToolList(assistantMessageId) {
window.toggleMcpToolList = toggleMcpToolList;
window.syncMcpToolsToggleButton = syncMcpToolsToggleButton;
+window.isProcessDetailsUserExpanded = isProcessDetailsUserExpanded;
+window.syncProcessDetailButtonLabels = syncProcessDetailButtonLabels;
window.ensureMcpCallSectionChrome = ensureMcpCallSectionChrome;
/** 将 MCP 工具按钮挂到独立工具列表,并批量解析工具名 */
@@ -6279,6 +6298,9 @@ const CONVERSATION_PROJECT_FILTER_NONE = '__none__';
const CONVERSATION_PROJECT_FILTER_SELECT_ID = 'conversation-project-filter';
const CONVERSATION_PROJECT_FILTER_CARET = '';
const BATCH_PROJECT_FILTER_SELECT_ID = 'batch-project-filter';
+const BATCH_GROUP_FILTER_SELECT_ID = 'batch-move-group-select';
+const BATCH_GROUP_HEADER_FILTER_SELECT_ID = 'batch-group-filter';
+const BATCH_GROUP_NONE = '__none__';
const projectFilterCustomSelectRegistry = {};
let projectFilterCustomSelectDocBound = false;
@@ -6467,6 +6489,117 @@ function syncProjectFilterCustomSelect(selectId) {
}
}
+function ensureSimpleCustomSelectOptionsUi(reg) {
+ if (reg.optionsList) return;
+ reg.dropdown.innerHTML = '';
+ const optionsList = document.createElement('div');
+ optionsList.className = 'conversation-project-filter-options';
+ reg.dropdown.appendChild(optionsList);
+ reg.optionsList = optionsList;
+}
+
+function renderSimpleCustomSelectOptions(reg) {
+ ensureSimpleCustomSelectOptionsUi(reg);
+ const { select, optionsList } = reg;
+ optionsList.innerHTML = '';
+ Array.prototype.forEach.call(select.options, (opt) => {
+ optionsList.appendChild(createProjectFilterOptionButton(opt.value, opt.textContent || '', select.value));
+ });
+}
+
+function syncSimpleCustomSelect(selectId) {
+ const reg = projectFilterCustomSelectRegistry[selectId];
+ if (!reg) return;
+ const { select, trigger } = reg;
+ const valueSpan = trigger.querySelector('.conversation-project-filter-value');
+ const selectedOpt = select.options[select.selectedIndex];
+ const selectedText = selectedOpt ? (selectedOpt.textContent || '') : '';
+ if (valueSpan) {
+ valueSpan.textContent = selectedText;
+ valueSpan.title = selectedText;
+ }
+}
+
+function initSimpleCustomSelect(selectId) {
+ const select = document.getElementById(selectId);
+ if (!select) return;
+ if (select.dataset.projectCustomSelect === '1') {
+ syncSimpleCustomSelect(selectId);
+ return;
+ }
+ select.dataset.projectCustomSelect = '1';
+ select.classList.add('conversation-project-filter-native');
+ select.tabIndex = -1;
+ select.setAttribute('aria-hidden', 'true');
+
+ const wrapper = document.createElement('div');
+ wrapper.className = 'conversation-project-filter-ui';
+
+ const trigger = document.createElement('button');
+ trigger.type = 'button';
+ trigger.className = 'conversation-project-filter-trigger';
+ trigger.setAttribute('aria-haspopup', 'listbox');
+ trigger.setAttribute('aria-expanded', 'false');
+ const valueSpan = document.createElement('span');
+ valueSpan.className = 'conversation-project-filter-value';
+ trigger.appendChild(valueSpan);
+ trigger.insertAdjacentHTML('beforeend', CONVERSATION_PROJECT_FILTER_CARET);
+
+ const dropdown = document.createElement('div');
+ dropdown.className = 'conversation-project-filter-dropdown';
+ dropdown.setAttribute('role', 'listbox');
+
+ const parent = select.parentNode;
+ parent.insertBefore(wrapper, select);
+ wrapper.appendChild(trigger);
+ wrapper.appendChild(dropdown);
+ wrapper.appendChild(select);
+
+ projectFilterCustomSelectRegistry[selectId] = { wrapper, trigger, dropdown, select };
+
+ trigger.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const open = wrapper.classList.contains('open');
+ closeAllProjectFilterCustomSelects();
+ if (!open) {
+ wrapper.classList.add('open');
+ trigger.setAttribute('aria-expanded', 'true');
+ renderSimpleCustomSelectOptions(projectFilterCustomSelectRegistry[selectId]);
+ }
+ });
+
+ dropdown.addEventListener('click', (e) => {
+ const opt = e.target.closest('.conversation-project-filter-option');
+ if (!opt) return;
+ e.stopPropagation();
+ const val = opt.getAttribute('data-value');
+ if (val === null) return;
+ if (select.value !== val) {
+ select.value = val;
+ select.dispatchEvent(new Event('change', { bubbles: true }));
+ }
+ closeProjectFilterCustomSelect(selectId);
+ syncSimpleCustomSelect(selectId);
+ });
+
+ if (!projectFilterCustomSelectDocBound) {
+ projectFilterCustomSelectDocBound = true;
+ document.addEventListener('click', closeAllProjectFilterCustomSelects);
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') closeAllProjectFilterCustomSelects();
+ });
+ }
+ syncSimpleCustomSelect(selectId);
+}
+
+function initBatchGroupCustomSelect() {
+ initSimpleCustomSelect(BATCH_GROUP_FILTER_SELECT_ID);
+}
+
+function initBatchGroupHeaderFilterCustomSelect() {
+ initSimpleCustomSelect(BATCH_GROUP_HEADER_FILTER_SELECT_ID);
+}
+
function initProjectFilterCustomSelect(selectId) {
const select = document.getElementById(selectId);
if (!select) return;
@@ -8507,6 +8640,22 @@ function getConversationProjectLabel(conv) {
return typeof window.t === 'function' ? window.t('batchManageModal.unknownProject') : '未知项目';
}
+function getConversationGroupId(conv) {
+ return (conv && conversationGroupMappingCache[conv.id]) || '';
+}
+
+function getConversationGroupLabel(conv) {
+ const groupId = getConversationGroupId(conv);
+ if (!groupId) {
+ return typeof window.t === 'function' ? window.t('batchManageModal.noGroup') : '无分组';
+ }
+ const group = Array.isArray(groupsCache) ? groupsCache.find(g => g.id === groupId) : null;
+ if (group) {
+ return `${group.icon || '📁'} ${group.name}`;
+ }
+ return typeof window.t === 'function' ? window.t('batchManageModal.unknownGroup') : '未知分组';
+}
+
async function prefetchProjectNamesForConversations(conversations) {
const missing = new Set();
for (const conv of conversations || []) {
@@ -8536,9 +8685,83 @@ async function refreshBatchProjectFilter() {
syncProjectFilterCustomSelect(BATCH_PROJECT_FILTER_SELECT_ID);
}
+async function refreshBatchGroupSelect() {
+ const sel = document.getElementById('batch-move-group-select');
+ if (!sel) return;
+
+ if (!Array.isArray(groupsCache) || groupsCache.length === 0) {
+ await loadGroups();
+ }
+
+ const saved = sel.value || BATCH_GROUP_NONE;
+ sel.innerHTML = '';
+
+ const noneOpt = document.createElement('option');
+ noneOpt.value = BATCH_GROUP_NONE;
+ noneOpt.textContent = projectFilterT('batchManageModal.noGroupOption', '无分组');
+ sel.appendChild(noneOpt);
+
+ (groupsCache || []).forEach((group) => {
+ const opt = document.createElement('option');
+ opt.value = group.id;
+ opt.textContent = `${group.icon || '📁'} ${group.name}`;
+ sel.appendChild(opt);
+ });
+
+ if (saved === BATCH_GROUP_NONE || (groupsCache || []).some((group) => group.id === saved)) {
+ sel.value = saved;
+ } else {
+ sel.value = BATCH_GROUP_NONE;
+ }
+ syncSimpleCustomSelect(BATCH_GROUP_FILTER_SELECT_ID);
+}
+
+function appendBatchGroupHeaderFilterNativeOptions(sel) {
+ const allLabel = projectFilterT('batchManageModal.filterAllGroups', '全部分组');
+ const ungroupedLabel = projectFilterT('batchManageModal.filterUngrouped', '无分组');
+ sel.innerHTML = '';
+ const allOpt = document.createElement('option');
+ allOpt.value = '';
+ allOpt.textContent = allLabel;
+ allOpt.setAttribute('data-i18n', 'batchManageModal.filterAllGroups');
+ sel.appendChild(allOpt);
+ const ungroupedOpt = document.createElement('option');
+ ungroupedOpt.value = BATCH_GROUP_NONE;
+ ungroupedOpt.textContent = ungroupedLabel;
+ ungroupedOpt.setAttribute('data-i18n', 'batchManageModal.filterUngrouped');
+ sel.appendChild(ungroupedOpt);
+}
+
+async function refreshBatchGroupHeaderFilter() {
+ const sel = document.getElementById(BATCH_GROUP_HEADER_FILTER_SELECT_ID);
+ if (!sel) return;
+
+ if (!Array.isArray(groupsCache) || groupsCache.length === 0) {
+ await loadGroups();
+ }
+
+ const saved = sel.value || '';
+ appendBatchGroupHeaderFilterNativeOptions(sel);
+
+ (groupsCache || []).forEach((group) => {
+ const opt = document.createElement('option');
+ opt.value = group.id;
+ opt.textContent = `${group.icon || '📁'} ${group.name}`;
+ sel.appendChild(opt);
+ });
+
+ if (saved === '' || saved === BATCH_GROUP_NONE || (groupsCache || []).some((group) => group.id === saved)) {
+ sel.value = saved;
+ } else {
+ sel.value = '';
+ }
+ syncSimpleCustomSelect(BATCH_GROUP_HEADER_FILTER_SELECT_ID);
+}
+
function getBatchFilteredConversations() {
const query = (document.getElementById('batch-search-input')?.value || '').trim().toLowerCase();
const projectFilter = (document.getElementById('batch-project-filter')?.value || '').trim();
+ const groupFilter = (document.getElementById(BATCH_GROUP_HEADER_FILTER_SELECT_ID)?.value || '').trim();
return allConversationsForBatch.filter((conv) => {
const pid = getConversationProjectId(conv);
if (projectFilter) {
@@ -8548,10 +8771,19 @@ function getBatchFilteredConversations() {
return false;
}
}
+ const gid = getConversationGroupId(conv);
+ if (groupFilter) {
+ if (groupFilter === BATCH_GROUP_NONE) {
+ if (gid) return false;
+ } else if (gid !== groupFilter) {
+ return false;
+ }
+ }
if (!query) return true;
const title = (conv.title || '').toLowerCase();
const projectName = getConversationProjectLabel(conv).toLowerCase();
- return title.includes(query) || projectName.includes(query);
+ const groupName = getConversationGroupLabel(conv).toLowerCase();
+ return title.includes(query) || projectName.includes(query) || groupName.includes(query);
});
}
@@ -8574,8 +8806,16 @@ async function showBatchManageModal() {
try {
initProjectFilterCustomSelect(BATCH_PROJECT_FILTER_SELECT_ID);
allConversationsForBatch = await fetchAllConversations('');
- await prefetchProjectNamesForConversations(allConversationsForBatch);
+ await Promise.all([
+ prefetchProjectNamesForConversations(allConversationsForBatch),
+ loadGroups(),
+ loadConversationGroupMapping(),
+ ]);
await refreshBatchProjectFilter();
+ initBatchGroupHeaderFilterCustomSelect();
+ await refreshBatchGroupHeaderFilter();
+ initBatchGroupCustomSelect();
+ await refreshBatchGroupSelect();
const sidebarFilter = getConversationProjectFilter();
const batchSel = document.getElementById('batch-project-filter');
if (batchSel && sidebarFilter && (
@@ -8591,8 +8831,10 @@ async function showBatchManageModal() {
} catch (error) {
console.error('加载对话列表失败:', error);
initProjectFilterCustomSelect(BATCH_PROJECT_FILTER_SELECT_ID);
+ initBatchGroupHeaderFilterCustomSelect();
+ initBatchGroupCustomSelect();
allConversationsForBatch = [];
- await refreshBatchProjectFilter();
+ await Promise.all([refreshBatchProjectFilter(), refreshBatchGroupHeaderFilter(), refreshBatchGroupSelect()]);
applyBatchConversationFilters();
openAppModal('batch-manage-modal', { focus: false });
}
@@ -8677,6 +8919,16 @@ function renderBatchConversations(filtered = null) {
project.classList.add('is-unbound');
}
+ const group = document.createElement('div');
+ group.className = 'batch-table-col-group';
+ const groupLabel = getConversationGroupLabel(conv);
+ const truncatedGroup = safeTruncateText(groupLabel, 24);
+ group.textContent = truncatedGroup;
+ group.title = groupLabel;
+ if (!getConversationGroupId(conv)) {
+ group.classList.add('is-unbound');
+ }
+
const time = document.createElement('div');
time.className = 'batch-table-col-time';
const dateObj = conv.updatedAt ? new Date(conv.updatedAt) : new Date();
@@ -8712,6 +8964,7 @@ function renderBatchConversations(filtered = null) {
row.appendChild(checkboxCol);
row.appendChild(name);
row.appendChild(project);
+ row.appendChild(group);
row.appendChild(time);
row.appendChild(action);
@@ -8759,6 +9012,133 @@ function syncSelectAllBatchCheckbox() {
}
}
+// 批量设置对话分组(选「无分组」即移出)
+async function applyBatchGroupChange() {
+ const checkboxes = document.querySelectorAll('.batch-conversation-checkbox:checked');
+ if (checkboxes.length === 0) {
+ alert(typeof window.t === 'function' ? window.t('batchManageModal.confirmGroupChangeNone') : '请先选择要操作的对话');
+ return;
+ }
+
+ const groupSelect = document.getElementById('batch-move-group-select');
+ const groupId = (groupSelect?.value || BATCH_GROUP_NONE).trim();
+
+ if (groupId === BATCH_GROUP_NONE) {
+ const items = Array.from(checkboxes).map((cb) => ({
+ id: cb.dataset.conversationId,
+ groupId: conversationGroupMappingCache[cb.dataset.conversationId] || '',
+ })).filter((item) => item.groupId);
+
+ if (items.length === 0) {
+ alert(typeof window.t === 'function' ? window.t('batchManageModal.confirmRemoveNoGroup') : '所选对话均未归属分组');
+ return;
+ }
+
+ const confirmMsg = typeof window.t === 'function'
+ ? window.t('batchManageModal.confirmRemoveN', { count: items.length })
+ : `确定将选中的 ${items.length} 条对话移出分组吗?`;
+ if (!confirm(confirmMsg)) {
+ return;
+ }
+
+ try {
+ for (const item of items) {
+ await apiFetch(`/api/groups/${item.groupId}/conversations/${item.id}`, {
+ method: 'DELETE',
+ });
+
+ delete conversationGroupMappingCache[item.id];
+ delete pendingGroupMappings[item.id];
+
+ if (currentConversationId === item.id) {
+ currentConversationGroupId = null;
+ }
+ }
+
+ await finishBatchGroupChangeAfterRemove();
+ } catch (error) {
+ console.error('批量移出分组失败:', error);
+ await loadConversationGroupMapping();
+ applyBatchConversationFilters();
+ const failedMsg = typeof window.t === 'function' ? window.t('batchManageModal.removeFailed') : '移出失败';
+ const unknownErr = typeof window.t === 'function' ? window.t('createGroupModal.unknownError') : '未知错误';
+ alert(failedMsg + ': ' + (error.message || unknownErr));
+ }
+ return;
+ }
+
+ const targetGroup = (groupsCache || []).find((group) => group.id === groupId);
+ const groupName = targetGroup ? targetGroup.name : groupId;
+ const confirmMsg = typeof window.t === 'function'
+ ? window.t('batchManageModal.confirmMoveN', { count: checkboxes.length, group: groupName })
+ : `确定将选中的 ${checkboxes.length} 条对话移动到「${groupName}」吗?`;
+ if (!confirm(confirmMsg)) {
+ return;
+ }
+
+ const ids = Array.from(checkboxes).map((cb) => cb.dataset.conversationId);
+
+ try {
+ for (const id of ids) {
+ await apiFetch('/api/groups/conversations', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ conversationId: id,
+ groupId,
+ }),
+ });
+
+ conversationGroupMappingCache[id] = groupId;
+ pendingGroupMappings[id] = groupId;
+
+ if (currentConversationId === id) {
+ currentConversationGroupId = groupId;
+ }
+ }
+
+ if (currentGroupId) {
+ await loadGroupConversations(currentGroupId);
+ }
+ await loadConversationsWithGroups();
+ await loadGroups();
+ resetBatchSelectionAfterGroupChange();
+ } catch (error) {
+ console.error('批量移动对话失败:', error);
+ await loadConversationGroupMapping();
+ applyBatchConversationFilters();
+ const failedMsg = typeof window.t === 'function' ? window.t('batchManageModal.moveFailed') : '移动失败';
+ const unknownErr = typeof window.t === 'function' ? window.t('createGroupModal.unknownError') : '未知错误';
+ alert(failedMsg + ': ' + (error.message || unknownErr));
+ }
+}
+
+function resetBatchSelectionAfterGroupChange() {
+ applyBatchConversationFilters();
+ const selectAll = document.getElementById('batch-select-all');
+ if (selectAll) {
+ selectAll.checked = false;
+ selectAll.indeterminate = false;
+ }
+}
+
+async function finishBatchGroupChangeAfterRemove() {
+ if (currentGroupId) {
+ await loadGroupConversations(currentGroupId);
+ }
+ await loadConversationGroupMapping();
+ await loadGroups();
+
+ const savedGroupId = currentGroupId;
+ currentGroupId = null;
+ await loadConversationsWithGroups();
+ currentGroupId = savedGroupId;
+
+ resetBatchSelectionAfterGroupChange();
+}
+
// 删除选中的对话
async function deleteSelectedConversations() {
const checkboxes = document.querySelectorAll('.batch-conversation-checkbox:checked');
@@ -8794,6 +9174,7 @@ async function deleteSelectedConversations() {
// 关闭批量管理模态框
function closeBatchManageModal() {
+ closeAllProjectFilterCustomSelects();
closeAppModal('batch-manage-modal');
const selectAll = document.getElementById('batch-select-all');
if (selectAll) {
@@ -8804,6 +9185,12 @@ function closeBatchManageModal() {
if (searchInput) searchInput.value = '';
const batchProj = document.getElementById('batch-project-filter');
if (batchProj) batchProj.value = '';
+ const batchGroupFilter = document.getElementById(BATCH_GROUP_HEADER_FILTER_SELECT_ID);
+ if (batchGroupFilter) batchGroupFilter.value = '';
+ syncSimpleCustomSelect(BATCH_GROUP_HEADER_FILTER_SELECT_ID);
+ const batchGroup = document.getElementById('batch-move-group-select');
+ if (batchGroup) batchGroup.value = BATCH_GROUP_NONE;
+ syncSimpleCustomSelect(BATCH_GROUP_FILTER_SELECT_ID);
allConversationsForBatch = [];
}
@@ -8885,6 +9272,12 @@ document.addEventListener('languagechange', function () {
}
});
}
+ if (typeof refreshBatchGroupSelect === 'function') {
+ refreshBatchGroupSelect().then(() => syncSimpleCustomSelect(BATCH_GROUP_FILTER_SELECT_ID));
+ }
+ if (typeof refreshBatchGroupHeaderFilter === 'function') {
+ refreshBatchGroupHeaderFilter();
+ }
// 侧边栏最近对话等列表的时间戳会随语言变化(24h/12h 等),重新拉列表以统一格式
if (typeof loadConversationsWithGroups === 'function') {
loadConversationsWithGroups();
diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js
index b9ebdba6..8643c815 100644
--- a/web/static/js/monitor.js
+++ b/web/static/js/monitor.js
@@ -1229,10 +1229,9 @@ function collapseAllProgressDetails(assistantMessageId, progressId) {
if (assistantMessageId) {
const detailsId = 'process-details-' + assistantMessageId;
const detailsContainer = document.getElementById(detailsId);
- if (detailsContainer) {
+ if (detailsContainer && detailsContainer.dataset.userExpanded !== '1') {
const timeline = detailsContainer.querySelector('.progress-timeline');
if (timeline) {
- // 确保移除expanded类(无论是否包含)
timeline.classList.remove('expanded');
document.querySelectorAll(`#${assistantMessageId} .process-detail-btn`).forEach((btn) => {
btn.innerHTML = '' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '';
@@ -1421,6 +1420,9 @@ function toggleProcessDetails(progressId, assistantMessageId) {
const detailsContainer = document.getElementById(detailsId);
if (!detailsContainer) return;
+ const expandT = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
+ const collapseT = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情';
+
// 懒加载:首次展开时才从后端拉取该条消息的过程详情
const maybeLazy = detailsContainer.dataset && detailsContainer.dataset.lazyNotLoaded === '1' && detailsContainer.dataset.loaded !== '1';
if (maybeLazy) {
@@ -1447,6 +1449,19 @@ function toggleProcessDetails(progressId, assistantMessageId) {
})
.finally(() => {
detailsContainer.dataset.loading = '0';
+ if (detailsContainer.dataset.userExpanded === '1') {
+ const tl = detailsContainer.querySelector('.progress-timeline');
+ if (tl) {
+ tl.classList.add('expanded');
+ }
+ if (typeof syncProcessDetailButtonLabels === 'function') {
+ syncProcessDetailButtonLabels(assistantMessageId, true);
+ } else {
+ document.querySelectorAll('#' + assistantMessageId + ' .process-detail-btn').forEach((btn) => {
+ btn.innerHTML = '' + collapseT + '';
+ });
+ }
+ }
});
}
}
@@ -1456,27 +1471,25 @@ function toggleProcessDetails(progressId, assistantMessageId) {
const timeline = detailsContainer.querySelector('.progress-timeline');
const detailBtns = document.querySelectorAll(`#${assistantMessageId} .process-detail-btn`);
- const expandT = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
- const collapseT = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情';
const setDetailBtnLabels = (label) => {
detailBtns.forEach((btn) => { btn.innerHTML = '' + label + ''; });
};
+ const setExpanded = (expanded) => {
+ if (!timeline) return;
+ if (expanded) {
+ timeline.classList.add('expanded');
+ detailsContainer.dataset.userExpanded = '1';
+ setDetailBtnLabels(collapseT);
+ } else {
+ timeline.classList.remove('expanded');
+ delete detailsContainer.dataset.userExpanded;
+ setDetailBtnLabels(expandT);
+ }
+ };
if (content && timeline) {
- if (timeline.classList.contains('expanded')) {
- timeline.classList.remove('expanded');
- setDetailBtnLabels(expandT);
- } else {
- timeline.classList.add('expanded');
- setDetailBtnLabels(collapseT);
- }
+ setExpanded(!timeline.classList.contains('expanded'));
} else if (timeline) {
- if (timeline.classList.contains('expanded')) {
- timeline.classList.remove('expanded');
- setDetailBtnLabels(expandT);
- } else {
- timeline.classList.add('expanded');
- setDetailBtnLabels(collapseT);
- }
+ setExpanded(!timeline.classList.contains('expanded'));
}
// 滚动到展开的详情位置(流式且用户上滑阅读时不抢主列表滚动)
@@ -3101,10 +3114,15 @@ function expandProcessDetailsTimeline(assistantMessageId) {
const timeline = detailsContainer.querySelector('.progress-timeline');
if (!timeline) return;
timeline.classList.add('expanded');
- const collapseT = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情';
- document.querySelectorAll('#' + hitlEscapeAttrSelector(assistantMessageId) + ' .process-detail-btn').forEach(function (btn) {
- btn.innerHTML = '' + collapseT + '';
- });
+ detailsContainer.dataset.userExpanded = '1';
+ if (typeof syncProcessDetailButtonLabels === 'function') {
+ syncProcessDetailButtonLabels(assistantMessageId, true);
+ } else {
+ const collapseT = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情';
+ document.querySelectorAll('#' + hitlEscapeAttrSelector(assistantMessageId) + ' .process-detail-btn').forEach(function (btn) {
+ btn.innerHTML = '' + collapseT + '';
+ });
+ }
setTimeout(function () {
if (window.CyberStrikeChatScroll && typeof window.CyberStrikeChatScroll.scrollIntoViewIfFollowing === 'function') {
window.CyberStrikeChatScroll.scrollIntoViewIfFollowing(detailsContainer, { behavior: 'smooth', block: 'nearest' });
diff --git a/web/templates/index.html b/web/templates/index.html
index 372da749..27e74d28 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -4533,6 +4533,10 @@
+
对话名称
项目
+ 对话分组
最近一次对话时间
操作
@@ -4558,6 +4563,12 @@