Add files via upload

This commit is contained in:
公明
2026-06-26 14:21:51 +08:00
committed by GitHub
parent 0168530084
commit 4a57574cf9
7 changed files with 755 additions and 52 deletions
+243 -6
View File
@@ -1615,9 +1615,34 @@ header {
.conversation-search-box {
position: relative;
margin-bottom: 10px;
}
.conversation-sidebar .sidebar-content {
padding: 10px 16px 16px;
}
.conversation-sidebar .conversation-search-box {
margin-top: 8px;
margin-bottom: 10px;
}
.conversation-sidebar .conversation-project-filter {
margin-bottom: 10px;
}
.conversation-sidebar .conversation-groups-section {
margin-bottom: 12px;
}
.conversation-sidebar .recent-conversations-section {
margin-bottom: 12px;
}
.conversation-sidebar .section-header {
margin-bottom: 8px;
}
.conversation-search-box input {
width: 100%;
padding: 8px 32px 8px 12px;
@@ -1668,6 +1693,170 @@ header {
height: 14px;
}
.conversation-project-filter {
margin-bottom: 12px;
min-width: 0;
}
.conversation-project-filter-label {
display: block;
font-size: 0.75rem;
font-weight: 500;
color: var(--text-muted);
margin-bottom: 4px;
padding: 0 2px;
}
.conversation-project-filter-native {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.conversation-project-filter-ui {
position: relative;
width: 100%;
min-width: 0;
}
.conversation-project-filter-trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
min-width: 0;
padding: 8px 10px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 0.875rem;
line-height: 1.25;
cursor: pointer;
font-family: inherit;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.conversation-project-filter-trigger:hover:not(:disabled) {
border-color: var(--accent-color);
}
.conversation-project-filter-ui.open .conversation-project-filter-trigger {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
}
.conversation-project-filter-ui.open {
z-index: 120;
}
.conversation-project-filter-value {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
.conversation-project-filter-caret {
flex-shrink: 0;
color: var(--text-secondary);
transition: transform 0.15s ease;
}
.conversation-project-filter-ui.open .conversation-project-filter-caret {
transform: rotate(180deg);
}
.conversation-project-filter-dropdown {
display: none;
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
z-index: 200;
max-height: 280px;
overflow-x: hidden;
overflow-y: auto;
padding: 4px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: var(--shadow-lg);
}
.conversation-project-filter-ui.open .conversation-project-filter-dropdown {
display: block;
}
.conversation-project-filter-option {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
min-width: 0;
padding: 8px 10px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--text-primary);
font-size: 0.8125rem;
font-family: inherit;
cursor: pointer;
text-align: left;
transition: background 0.12s ease, color 0.12s ease;
}
.conversation-project-filter-option:hover {
background: var(--bg-secondary);
}
.conversation-project-filter-option.is-selected {
background: rgba(0, 102, 255, 0.08);
color: var(--accent-color);
font-weight: 500;
}
.conversation-project-filter-check {
width: 14px;
flex-shrink: 0;
opacity: 0;
font-size: 0.75rem;
line-height: 1;
color: var(--accent-color);
}
.conversation-project-filter-option.is-selected .conversation-project-filter-check {
opacity: 1;
}
.conversation-project-filter-option-label {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.conversation-item-project-badge {
font-size: 0.6875rem;
color: var(--text-muted);
margin-top: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.3;
}
.conversations-list {
display: flex;
flex-direction: column;
@@ -11196,6 +11385,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
.conversation-groups-section,
.recent-conversations-section {
margin-bottom: 24px;
min-width: 0;
}
.conversation-groups-section:last-child,
@@ -11209,6 +11399,8 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
justify-content: space-between;
margin-bottom: 12px;
padding: 0 8px;
min-width: 0;
gap: 8px;
}
.section-header-actions {
@@ -11337,6 +11529,21 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
letter-spacing: 0.5px;
}
.recent-conversations-section .section-title {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.recent-conversations-section .section-title.section-title--filtered {
text-transform: none;
letter-spacing: normal;
font-size: 0.875rem;
color: var(--text-primary);
}
.add-group-btn,
.batch-manage-btn {
width: 24px;
@@ -11729,7 +11936,7 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
/* 批量管理模态框 */
.batch-manage-modal-content {
max-width: 800px;
max-width: 920px;
width: 90vw;
display: flex;
flex-direction: column;
@@ -11739,7 +11946,23 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
.batch-manage-header-actions {
display: flex;
align-items: center;
gap: 12px;
gap: 10px;
min-width: 0;
}
.batch-manage-header-actions .conversation-project-filter-ui {
width: 148px;
min-width: 108px;
flex-shrink: 0;
}
.batch-manage-header-actions .conversation-project-filter-trigger {
font-size: 0.8125rem;
padding: 8px 10px;
}
.batch-manage-modal-content .conversation-project-filter-ui.open {
z-index: 400;
}
.batch-search-box {
@@ -11783,8 +12006,8 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
.batch-table-header {
display: grid;
grid-template-columns: 40px 1fr 180px 80px;
gap: 16px;
grid-template-columns: 40px minmax(0, 1.2fr) minmax(0, 0.9fr) 160px 72px;
gap: 12px;
padding: 12px 16px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
@@ -11802,8 +12025,8 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
.batch-conversation-row {
display: grid;
grid-template-columns: 40px 1fr 180px 80px;
gap: 16px;
grid-template-columns: 40px minmax(0, 1.2fr) minmax(0, 0.9fr) 160px 72px;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
align-items: center;
@@ -11830,6 +12053,20 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
/* 完全依赖JavaScript截断,禁用CSS的ellipsis以避免在UTF-8多字节字符中间截断 */
}
.batch-table-col-project {
font-size: 0.8125rem;
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.batch-table-col-project.is-unbound {
color: var(--text-muted);
font-style: italic;
opacity: 0.85;
}
.batch-table-col-time {
font-size: 0.875rem;
color: var(--text-muted);
+10
View File
@@ -499,6 +499,13 @@
"conversationGroups": "Conversation groups",
"addGroup": "New group",
"recentConversations": "Recent conversations",
"filterByProject": "Filter by project",
"filterAllProjects": "All projects",
"filterUnboundProjects": "Unbound",
"projectConversationsTitle": "{{name}} · Conversations",
"unboundConversationsTitle": "Unbound conversations",
"noProjectConversations": "No conversations in this project",
"noUnboundConversations": "No unbound conversations",
"sortConversations": "Sort",
"sortByCreatedAt": "Created time",
"sortByUpdatedAt": "Updated time",
@@ -2527,6 +2534,9 @@
"title": "Manage conversations · {{count}} total",
"searchPlaceholder": "Search history",
"conversationName": "Conversation name",
"project": "Project",
"noProject": "No project",
"filterByProject": "Filter by project",
"lastTime": "Last activity",
"action": "Action",
"selectAll": "Select all",
+10
View File
@@ -487,6 +487,13 @@
"conversationGroups": "对话分组",
"addGroup": "新建分组",
"recentConversations": "最近对话",
"filterByProject": "按项目筛选",
"filterAllProjects": "全部项目",
"filterUnboundProjects": "未绑定项目",
"projectConversationsTitle": "{{name}} · 对话",
"unboundConversationsTitle": "未绑定项目",
"noProjectConversations": "该项目暂无对话",
"noUnboundConversations": "暂无未绑定项目的对话",
"sortConversations": "排序",
"sortByCreatedAt": "创建时间",
"sortByUpdatedAt": "更新时间",
@@ -2515,6 +2522,9 @@
"title": "管理对话记录·共{{count}}条",
"searchPlaceholder": "搜索历史记录",
"conversationName": "对话名称",
"project": "项目",
"noProject": "无项目",
"filterByProject": "按项目筛选",
"lastTime": "最近一次对话时间",
"action": "操作",
"selectAll": "全选",
+425 -34
View File
@@ -3322,6 +3322,18 @@ function createConversationListItem(conversation) {
title.title = titleText; // 设置完整标题以便悬停查看
contentWrapper.appendChild(title);
if (!getConversationProjectFilter()) {
const pid = conversation.projectId || conversation.project_id || '';
const projectName = pid && window.projectNameById ? window.projectNameById[pid] : '';
if (projectName) {
const badge = document.createElement('div');
badge.className = 'conversation-item-project-badge';
badge.textContent = projectName;
badge.title = projectName;
contentWrapper.appendChild(badge);
}
}
const time = document.createElement('div');
time.className = 'conversation-time';
time.textContent = conversation._timeText || formatConversationTimestamp(conversation._time || new Date());
@@ -3867,14 +3879,7 @@ async function deleteConversation(conversationId, skipConfirm = false) {
const batchModal = document.getElementById('batch-manage-modal');
if (batchModal && isAppModalOpen('batch-manage-modal')) {
allConversationsForBatch = allConversationsForBatch.filter(c => c.id !== conversationId);
updateBatchManageTitle(allConversationsForBatch.length);
const searchInput = document.getElementById('batch-search-input');
const query = searchInput ? searchInput.value : '';
if (query && query.trim()) {
filterBatchConversations(query);
} else {
renderBatchConversations();
}
applyBatchConversationFilters();
}
// 通知其他模块(如 WebShell AI 助手)同步删除,保持列表一致
@@ -6075,6 +6080,266 @@ let pendingGroupMappings = {}; // 待保留的分组映射(用于处理后端A
let conversationsListLoadSeq = 0; // 对话列表加载序号,避免并发请求导致重复渲染
const CONVERSATIONS_PAGE_SIZE_KEY = 'cyberstrike.conversations_page_size';
const CONVERSATIONS_SORT_KEY = 'cyberstrike.conversations_sort_by';
const CONVERSATIONS_PROJECT_FILTER_KEY = 'cyberstrike.conversations_project_filter';
const CONVERSATION_PROJECT_FILTER_NONE = '__none__';
const CONVERSATION_PROJECT_FILTER_SELECT_ID = 'conversation-project-filter';
const CONVERSATION_PROJECT_FILTER_CARET = '<svg class="conversation-project-filter-caret" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
const BATCH_PROJECT_FILTER_SELECT_ID = 'batch-project-filter';
const projectFilterCustomSelectRegistry = {};
let projectFilterCustomSelectDocBound = false;
function closeProjectFilterCustomSelect(selectId) {
const reg = projectFilterCustomSelectRegistry[selectId];
if (!reg || !reg.wrapper) return;
reg.wrapper.classList.remove('open');
if (reg.trigger) reg.trigger.setAttribute('aria-expanded', 'false');
}
function closeAllProjectFilterCustomSelects() {
Object.keys(projectFilterCustomSelectRegistry).forEach(closeProjectFilterCustomSelect);
}
function syncProjectFilterCustomSelect(selectId) {
const reg = projectFilterCustomSelectRegistry[selectId];
if (!reg) return;
const { select, dropdown, trigger } = reg;
const valueSpan = trigger.querySelector('.conversation-project-filter-value');
dropdown.innerHTML = '';
Array.prototype.forEach.call(select.options, (opt) => {
const item = document.createElement('button');
item.type = 'button';
item.className = 'conversation-project-filter-option';
item.setAttribute('role', 'option');
item.setAttribute('data-value', opt.value);
const labelText = opt.textContent || '';
item.title = labelText;
if (opt.value === select.value) {
item.classList.add('is-selected');
item.setAttribute('aria-selected', 'true');
} else {
item.setAttribute('aria-selected', 'false');
}
const check = document.createElement('span');
check.className = 'conversation-project-filter-check';
check.setAttribute('aria-hidden', 'true');
check.textContent = '✓';
const label = document.createElement('span');
label.className = 'conversation-project-filter-option-label';
label.textContent = labelText;
label.title = labelText;
item.appendChild(check);
item.appendChild(label);
dropdown.appendChild(item);
});
const selectedOpt = select.options[select.selectedIndex];
const selectedText = selectedOpt ? (selectedOpt.textContent || '') : '';
if (valueSpan) {
valueSpan.textContent = selectedText;
valueSpan.title = selectedText;
}
}
function initProjectFilterCustomSelect(selectId) {
const select = document.getElementById(selectId);
if (!select) return;
if (select.dataset.projectCustomSelect === '1') {
syncProjectFilterCustomSelect(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');
}
});
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);
syncProjectFilterCustomSelect(selectId);
});
if (!projectFilterCustomSelectDocBound) {
projectFilterCustomSelectDocBound = true;
document.addEventListener('click', closeAllProjectFilterCustomSelects);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeAllProjectFilterCustomSelects();
});
}
syncProjectFilterCustomSelect(selectId);
}
function syncConversationProjectCustomSelect() {
syncProjectFilterCustomSelect(CONVERSATION_PROJECT_FILTER_SELECT_ID);
}
function initConversationProjectCustomSelect() {
initProjectFilterCustomSelect(CONVERSATION_PROJECT_FILTER_SELECT_ID);
}
function getConversationProjectFilter() {
try {
return localStorage.getItem(CONVERSATIONS_PROJECT_FILTER_KEY) || '';
} catch (e) {
return '';
}
}
function setConversationProjectFilter(projectId) {
const value = (projectId || '').trim();
try {
if (value) localStorage.setItem(CONVERSATIONS_PROJECT_FILTER_KEY, value);
else localStorage.removeItem(CONVERSATIONS_PROJECT_FILTER_KEY);
} catch (e) { /* ignore */ }
const sel = document.getElementById('conversation-project-filter');
if (sel && sel.value !== value) sel.value = value;
syncConversationProjectCustomSelect();
updateConversationSidebarFilterUI();
}
function isValidConversationProjectFilter(projectId) {
if (!projectId) return true;
if (projectId === CONVERSATION_PROJECT_FILTER_NONE) return true;
const map = window.projectNameById;
if (!map || typeof map !== 'object') return true;
return Object.prototype.hasOwnProperty.call(map, projectId);
}
async function refreshConversationProjectFilter() {
const sel = document.getElementById('conversation-project-filter');
if (!sel) return;
const saved = getConversationProjectFilter();
let projects = [];
if (typeof window.ensureProjectsLoaded === 'function') {
try {
const list = await window.ensureProjectsLoaded();
projects = (list || []).filter((p) => p && p.id && p.status !== 'archived');
} catch (e) { /* ignore */ }
}
if (!projects.length) {
try {
const res = await apiFetch('/api/projects?status=active&limit=200');
if (res.ok) {
const data = await res.json();
const items = data.projects || data.items || (Array.isArray(data) ? data : []);
projects = items.filter((p) => p && p.id);
if (typeof window.rebuildProjectNameMap === 'function') {
window.rebuildProjectNameMap(items);
}
}
} catch (e) { /* ignore */ }
}
const tFn = typeof window.t === 'function' ? window.t.bind(window) : null;
const allLabel = tFn ? tFn('chat.filterAllProjects') : '全部项目';
const unboundLabel = tFn ? tFn('chat.filterUnboundProjects') : '未绑定项目';
sel.innerHTML = '';
const allOpt = document.createElement('option');
allOpt.value = '';
allOpt.textContent = allLabel;
allOpt.setAttribute('data-i18n', 'chat.filterAllProjects');
sel.appendChild(allOpt);
const unboundOpt = document.createElement('option');
unboundOpt.value = CONVERSATION_PROJECT_FILTER_NONE;
unboundOpt.textContent = unboundLabel;
unboundOpt.setAttribute('data-i18n', 'chat.filterUnboundProjects');
sel.appendChild(unboundOpt);
projects
.slice()
.sort((a, b) => (a.name || a.id || '').localeCompare(b.name || b.id || '', undefined, { sensitivity: 'base' }))
.forEach((p) => {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.name || p.id;
sel.appendChild(opt);
});
const normalized = isValidConversationProjectFilter(saved) ? saved : '';
if (normalized !== saved) setConversationProjectFilter(normalized);
sel.value = normalized;
syncConversationProjectCustomSelect();
updateConversationSidebarFilterUI();
}
function onConversationProjectFilterChange(projectId) {
setConversationProjectFilter(projectId || '');
conversationsPagination.page = 1;
loadConversationsWithGroups(conversationsSearchQuery);
}
function updateConversationSidebarFilterUI() {
const groupsSection = document.querySelector('.conversation-groups-section');
const titleEl = document.querySelector('.recent-conversations-section .section-title');
const filter = getConversationProjectFilter();
const hasSearch = !!(conversationsSearchQuery && conversationsSearchQuery.trim());
if (groupsSection) {
groupsSection.hidden = !!filter || hasSearch;
}
if (!titleEl) return;
const tFn = typeof window.t === 'function' ? window.t.bind(window) : null;
if (filter && filter !== CONVERSATION_PROJECT_FILTER_NONE) {
const name = (window.projectNameById && window.projectNameById[filter]) || filter;
const fullTitle = tFn ? tFn('chat.projectConversationsTitle', { name }) : `${name} · 对话`;
titleEl.textContent = fullTitle;
titleEl.title = fullTitle;
titleEl.classList.add('section-title--filtered');
titleEl.removeAttribute('data-i18n');
} else if (filter === CONVERSATION_PROJECT_FILTER_NONE) {
const fullTitle = tFn ? tFn('chat.unboundConversationsTitle') : '未绑定项目';
titleEl.textContent = fullTitle;
titleEl.title = fullTitle;
titleEl.classList.add('section-title--filtered');
titleEl.setAttribute('data-i18n', 'chat.unboundConversationsTitle');
} else {
titleEl.classList.remove('section-title--filtered');
titleEl.removeAttribute('title');
titleEl.setAttribute('data-i18n', 'chat.recentConversations');
if (tFn) titleEl.textContent = tFn('chat.recentConversations');
}
}
window.onConversationProjectBindingChanged = function onConversationProjectBindingChanged() {
loadConversationsWithGroups(conversationsSearchQuery);
};
function getConversationSortBy() {
try {
@@ -6252,6 +6517,13 @@ async function fetchAllConversations(searchQuery) {
}
function getConversationListEmptyHtml() {
const filter = getConversationProjectFilter();
if (filter && filter !== CONVERSATION_PROJECT_FILTER_NONE) {
return '<div class="conversations-list-empty" data-i18n="chat.noProjectConversations"></div>';
}
if (filter === CONVERSATION_PROJECT_FILTER_NONE) {
return '<div class="conversations-list-empty" data-i18n="chat.noUnboundConversations"></div>';
}
return '<div class="conversations-list-empty" data-i18n="chat.noHistoryConversations"></div>';
}
@@ -6428,11 +6700,16 @@ async function loadConversationsWithGroups(searchQuery = '') {
if (conversationSortBy === 'created_at') {
convParams.set('sort_by', 'created_at');
}
const projectFilter = getConversationProjectFilter();
if (projectFilter) {
convParams.set('project_id', projectFilter);
}
if (searchQuery && searchQuery.trim()) {
convParams.set('search', searchQuery.trim());
} else {
} else if (!projectFilter) {
convParams.set('exclude_grouped', 'true');
}
updateConversationSidebarFilterUI();
const url = `/api/conversations?${convParams}`;
const [,, response] = await Promise.all([
loadGroups(),
@@ -6488,6 +6765,7 @@ async function loadConversationsWithGroups(searchQuery = '') {
const pinnedConvs = [];
const normalConvs = [];
const hasSearchQuery = searchQuery && searchQuery.trim();
const hasProjectFilter = !!getConversationProjectFilter();
uniqueConversations.forEach(conv => {
// 如果有搜索关键词,显示所有匹配的对话(全局搜索,包括分组中的)
@@ -6501,6 +6779,16 @@ async function loadConversationsWithGroups(searchQuery = '') {
return;
}
// 按项目筛选时展示该项目下全部对话(含分组内)
if (hasProjectFilter) {
if (conv.pinned) {
pinnedConvs.push(conv);
} else {
normalConvs.push(conv);
}
return;
}
// 如果没有搜索关键词,使用原有逻辑
// "最近对话"列表应该只显示不在任何分组中的对话
// 无论是否在分组详情页,都不应该在"最近对话"中显示分组中的对话
@@ -7731,6 +8019,84 @@ function closeContextMenu() {
// 显示批量管理模态框
let allConversationsForBatch = [];
function getConversationProjectId(conv) {
return (conv?.projectId || conv?.project_id || '').trim();
}
function getConversationProjectLabel(conv) {
const pid = getConversationProjectId(conv);
if (!pid) {
return typeof window.t === 'function' ? window.t('batchManageModal.noProject') : '无项目';
}
return (window.projectNameById && window.projectNameById[pid]) || pid;
}
async function refreshBatchProjectFilter() {
const sel = document.getElementById('batch-project-filter');
if (!sel) return;
const saved = sel.value || '';
if (typeof window.ensureProjectsLoaded === 'function') {
try {
await window.ensureProjectsLoaded();
} catch (e) { /* ignore */ }
}
const tFn = typeof window.t === 'function' ? window.t.bind(window) : null;
const allLabel = tFn ? tFn('chat.filterAllProjects') : '全部项目';
const unboundLabel = tFn ? tFn('chat.filterUnboundProjects') : '未绑定项目';
sel.innerHTML = '';
const allOpt = document.createElement('option');
allOpt.value = '';
allOpt.textContent = allLabel;
allOpt.setAttribute('data-i18n', 'chat.filterAllProjects');
sel.appendChild(allOpt);
const unboundOpt = document.createElement('option');
unboundOpt.value = CONVERSATION_PROJECT_FILTER_NONE;
unboundOpt.textContent = unboundLabel;
unboundOpt.setAttribute('data-i18n', 'chat.filterUnboundProjects');
sel.appendChild(unboundOpt);
const source = window.projectNameById ? Object.keys(window.projectNameById) : [];
source
.sort((a, b) => {
const na = (window.projectNameById[a] || a).toLowerCase();
const nb = (window.projectNameById[b] || b).toLowerCase();
return na.localeCompare(nb);
})
.forEach((id) => {
const opt = document.createElement('option');
opt.value = id;
opt.textContent = window.projectNameById[id] || id;
sel.appendChild(opt);
});
const valid = !saved || saved === CONVERSATION_PROJECT_FILTER_NONE || (window.projectNameById && window.projectNameById[saved]);
sel.value = valid ? saved : '';
syncProjectFilterCustomSelect(BATCH_PROJECT_FILTER_SELECT_ID);
}
function getBatchFilteredConversations() {
const query = (document.getElementById('batch-search-input')?.value || '').trim().toLowerCase();
const projectFilter = (document.getElementById('batch-project-filter')?.value || '').trim();
return allConversationsForBatch.filter((conv) => {
const pid = getConversationProjectId(conv);
if (projectFilter) {
if (projectFilter === CONVERSATION_PROJECT_FILTER_NONE) {
if (pid) return false;
} else if (pid !== projectFilter) {
return false;
}
}
if (!query) return true;
const title = (conv.title || '').toLowerCase();
const projectName = getConversationProjectLabel(conv).toLowerCase();
return title.includes(query) || projectName.includes(query);
});
}
function applyBatchConversationFilters() {
const filtered = getBatchFilteredConversations();
updateBatchManageTitle(filtered.length);
renderBatchConversations(filtered);
}
// 更新批量管理模态框标题(含条数),支持 i18n;count 为当前条数
function updateBatchManageTitle(count) {
const titleEl = document.getElementById('batch-manage-title');
@@ -7742,19 +8108,27 @@ function updateBatchManageTitle(count) {
async function showBatchManageModal() {
try {
initProjectFilterCustomSelect(BATCH_PROJECT_FILTER_SELECT_ID);
allConversationsForBatch = await fetchAllConversations('');
const modal = document.getElementById('batch-manage-modal');
updateBatchManageTitle(allConversationsForBatch.length);
renderBatchConversations();
await refreshBatchProjectFilter();
const sidebarFilter = getConversationProjectFilter();
const batchSel = document.getElementById('batch-project-filter');
if (batchSel && sidebarFilter && (
sidebarFilter === CONVERSATION_PROJECT_FILTER_NONE ||
(window.projectNameById && window.projectNameById[sidebarFilter])
)) {
batchSel.value = sidebarFilter;
}
const searchInput = document.getElementById('batch-search-input');
if (searchInput) searchInput.value = '';
applyBatchConversationFilters();
openAppModal('batch-manage-modal', { focus: false });
} catch (error) {
console.error('加载对话列表失败:', error);
// 错误时使用空数组,不显示错误提示(更友好的用户体验)
initProjectFilterCustomSelect(BATCH_PROJECT_FILTER_SELECT_ID);
allConversationsForBatch = [];
updateBatchManageTitle(0);
renderBatchConversations();
await refreshBatchProjectFilter();
applyBatchConversationFilters();
openAppModal('batch-manage-modal', { focus: false });
}
}
@@ -7817,15 +8191,27 @@ function renderBatchConversations(filtered = null) {
checkbox.dataset.conversationId = conv.id;
checkbox.addEventListener('change', syncSelectAllBatchCheckbox);
const checkboxCol = document.createElement('div');
checkboxCol.className = 'batch-table-col-checkbox';
checkboxCol.appendChild(checkbox);
const name = document.createElement('div');
name.className = 'batch-table-col-name';
const originalTitle = conv.title || (typeof window.t === 'function' ? window.t('batchManageModal.unnamedConversation') : '未命名对话');
// 使用安全截断函数,限制最大长度为45个字符(留出空间显示省略号)
const truncatedTitle = safeTruncateText(originalTitle, 45);
const truncatedTitle = safeTruncateText(originalTitle, 36);
name.textContent = truncatedTitle;
// 设置title属性以显示完整文本(鼠标悬停时)
name.title = originalTitle;
const project = document.createElement('div');
project.className = 'batch-table-col-project';
const projectLabel = getConversationProjectLabel(conv);
const truncatedProject = safeTruncateText(projectLabel, 28);
project.textContent = truncatedProject;
project.title = projectLabel;
if (!getConversationProjectId(conv)) {
project.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();
@@ -7858,8 +8244,9 @@ function renderBatchConversations(filtered = null) {
};
action.appendChild(deleteBtn);
row.appendChild(checkbox);
row.appendChild(checkboxCol);
row.appendChild(name);
row.appendChild(project);
row.appendChild(time);
row.appendChild(action);
@@ -7870,18 +8257,8 @@ function renderBatchConversations(filtered = null) {
}
// 筛选批量管理对话
function filterBatchConversations(query) {
if (!query || !query.trim()) {
renderBatchConversations();
return;
}
const filtered = allConversationsForBatch.filter(conv => {
const title = (conv.title || '').toLowerCase();
return title.includes(query.toLowerCase());
});
renderBatchConversations(filtered);
function filterBatchConversations() {
applyBatchConversationFilters();
}
// 全选/取消全选
@@ -7958,6 +8335,10 @@ function closeBatchManageModal() {
selectAll.checked = false;
selectAll.indeterminate = false;
}
const searchInput = document.getElementById('batch-search-input');
if (searchInput) searchInput.value = '';
const batchProj = document.getElementById('batch-project-filter');
if (batchProj) batchProj.value = '';
allConversationsForBatch = [];
}
@@ -8030,7 +8411,7 @@ document.addEventListener('languagechange', function () {
refreshChatPanelI18n();
const modal = document.getElementById('batch-manage-modal');
if (isAppModalOpen('batch-manage-modal')) {
updateBatchManageTitle(allConversationsForBatch.length);
refreshBatchProjectFilter().then(() => applyBatchConversationFilters());
}
// 侧边栏最近对话等列表的时间戳会随语言变化(24h/12h 等),重新拉列表以统一格式
if (typeof loadConversationsWithGroups === 'function') {
@@ -8962,6 +9343,8 @@ function clearGroupSearch() {
// 初始化时加载分组
document.addEventListener('DOMContentLoaded', async () => {
updateConversationSortMenuUI();
initConversationProjectCustomSelect();
await refreshConversationProjectFilter();
await loadGroups();
await loadConversationsWithGroups();
@@ -9018,8 +9401,16 @@ document.addEventListener('DOMContentLoaded', async () => {
});
});
async function refreshAllProjectFilterSelects() {
await refreshConversationProjectFilter();
await refreshBatchProjectFilter();
}
// 顶层 async function 不会自动挂到 windowhitl 等脚本依赖 window.loadConversation
if (typeof window !== 'undefined') {
window.loadConversation = loadConversation;
window.startNewConversation = startNewConversation;
window.refreshConversationProjectFilter = refreshConversationProjectFilter;
window.refreshAllProjectFilterSelects = refreshAllProjectFilterSelects;
window.onConversationProjectFilterChange = onConversationProjectFilterChange;
}
+42 -11
View File
@@ -3547,6 +3547,7 @@ const monitorState = {
timelineLoading: false,
lastFetchedAt: null,
retentionDays: 0,
selectedExecutions: new Set(),
pagination: {
page: 1,
pageSize: (() => {
@@ -5063,10 +5064,12 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
const terminateBtn = status === 'running'
? `<button type="button" class="btn-secondary btn-monitor-abort" onclick="cancelMCPToolExecution('${rawExecId.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}')">${escapeHtml(terminateLabel)}</button>`
: '';
const jsExecId = rawExecId.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const isSelected = monitorState.selectedExecutions.has(rawExecId);
return `
<tr>
<td>
<input type="checkbox" class="monitor-execution-checkbox" value="${executionId}" onchange="updateBatchActionsState()" />
<input type="checkbox" class="monitor-execution-checkbox" value="${executionId}" ${isSelected ? 'checked' : ''} onchange="toggleExecutionSelection('${jsExecId}', this.checked)" />
</td>
<td>${toolName}</td>
<td><span class="${statusClass}">${escapeHtml(statusLabel)}</span></td>
@@ -5212,6 +5215,8 @@ async function deleteExecution(executionId) {
throw new Error(error.error || deleteFailedMsg);
}
monitorState.selectedExecutions.delete(executionId);
// 删除成功后刷新当前页面
const currentPage = monitorState.pagination.page;
await refreshMonitorPanel(currentPage);
@@ -5225,10 +5230,22 @@ async function deleteExecution(executionId) {
}
}
// 切换单条执行记录选中状态(持久化到 monitorState,避免轮询刷新后丢失)
function toggleExecutionSelection(executionId, selected) {
if (!executionId) {
return;
}
if (selected) {
monitorState.selectedExecutions.add(executionId);
} else {
monitorState.selectedExecutions.delete(executionId);
}
updateBatchActionsState();
}
// 更新批量操作状态
function updateBatchActionsState() {
const checkboxes = document.querySelectorAll('.monitor-execution-checkbox:checked');
const selectedCount = checkboxes.length;
const selectedCount = monitorState.selectedExecutions.size;
const batchActions = document.getElementById('monitor-batch-actions');
const selectedCountSpan = document.getElementById('monitor-selected-count');
@@ -5245,13 +5262,18 @@ function updateBatchActionsState() {
selectedCountSpan.textContent = typeof window.t === 'function' ? window.t('mcp.selectedCount', { count: selectedCount }) : '已选择 ' + selectedCount + ' 项';
}
// 更新全选复选框状态
// 更新全选复选框状态(仅反映当前页)
const selectAllCheckbox = document.getElementById('monitor-select-all');
if (selectAllCheckbox) {
const allCheckboxes = document.querySelectorAll('.monitor-execution-checkbox');
const allChecked = allCheckboxes.length > 0 && Array.from(allCheckboxes).every(cb => cb.checked);
selectAllCheckbox.checked = allChecked;
selectAllCheckbox.indeterminate = selectedCount > 0 && selectedCount < allCheckboxes.length;
if (allCheckboxes.length === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else {
const checkedOnPage = Array.from(allCheckboxes).filter(cb => monitorState.selectedExecutions.has(cb.value)).length;
selectAllCheckbox.checked = checkedOnPage === allCheckboxes.length;
selectAllCheckbox.indeterminate = checkedOnPage > 0 && checkedOnPage < allCheckboxes.length;
}
}
}
@@ -5260,6 +5282,11 @@ function toggleSelectAll(checkbox) {
const checkboxes = document.querySelectorAll('.monitor-execution-checkbox');
checkboxes.forEach(cb => {
cb.checked = checkbox.checked;
if (checkbox.checked) {
monitorState.selectedExecutions.add(cb.value);
} else {
monitorState.selectedExecutions.delete(cb.value);
}
});
updateBatchActionsState();
}
@@ -5269,6 +5296,7 @@ function selectAllExecutions() {
const checkboxes = document.querySelectorAll('.monitor-execution-checkbox');
checkboxes.forEach(cb => {
cb.checked = true;
monitorState.selectedExecutions.add(cb.value);
});
const selectAllCheckbox = document.getElementById('monitor-select-all');
if (selectAllCheckbox) {
@@ -5284,6 +5312,7 @@ function deselectAllExecutions() {
checkboxes.forEach(cb => {
cb.checked = false;
});
monitorState.selectedExecutions.clear();
const selectAllCheckbox = document.getElementById('monitor-select-all');
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
@@ -5294,14 +5323,12 @@ function deselectAllExecutions() {
// 批量删除执行记录
async function batchDeleteExecutions() {
const checkboxes = document.querySelectorAll('.monitor-execution-checkbox:checked');
if (checkboxes.length === 0) {
const ids = Array.from(monitorState.selectedExecutions);
if (ids.length === 0) {
const selectFirstMsg = typeof window.t === 'function' ? window.t('mcpMonitor.selectExecFirst') : '请先选择要删除的执行记录';
alert(selectFirstMsg);
return;
}
const ids = Array.from(checkboxes).map(cb => cb.value);
const count = ids.length;
const batchConfirmMsg = typeof window.t === 'function' ? window.t('mcpMonitor.batchDeleteConfirm', { count: count }) : `确定要删除选中的 ${count} 条执行记录吗?此操作不可恢复。`;
if (!confirm(batchConfirmMsg)) {
@@ -5325,6 +5352,10 @@ async function batchDeleteExecutions() {
const result = await response.json().catch(() => ({}));
const deletedCount = result.deleted || count;
ids.forEach(function (id) {
monitorState.selectedExecutions.delete(id);
});
// 删除成功后刷新当前页面
const currentPage = monitorState.pagination.page;
+10
View File
@@ -293,6 +293,9 @@ async function ensureProjectsLoaded(force) {
projectsCacheAll = list;
rebuildProjectNameMap(projectsCacheAll);
_projectsListReady = true;
if (typeof window.refreshConversationProjectFilter === 'function') {
window.refreshConversationProjectFilter();
}
return projectsCacheAll;
})
.catch((e) => {
@@ -371,6 +374,9 @@ async function loadProjectsList() {
if (typeof refreshVulnerabilityProjectFilter === 'function') {
refreshVulnerabilityProjectFilter();
}
if (typeof window.refreshAllProjectFilterSelects === 'function') {
await window.refreshAllProjectFilterSelects();
}
}
function projectInitial(name) {
@@ -2198,6 +2204,9 @@ async function applyChatProjectSelection(projectId) {
setActiveProjectId(projectId);
}
updateChatProjectButtonLabel();
if (typeof window.onConversationProjectBindingChanged === 'function') {
window.onConversationProjectBindingChanged(projectId);
}
}
/** 对话页项目选择器:同步按钮文案;若浮层已打开则刷新列表 */
@@ -2326,3 +2335,4 @@ window.focusProjectFactGraphEdge = focusProjectFactGraphEdge;
window.toggleProjectFactGraphConnectMode = toggleProjectFactGraphConnectMode;
window.rebuildProjectNameMap = rebuildProjectNameMap;
window.projectNameById = projectNameById;
window.ensureProjectsLoaded = ensureProjectsLoaded;
+15 -1
View File
@@ -778,7 +778,7 @@
</div>
<div class="sidebar-content">
<!-- 全局搜索 -->
<div class="conversation-search-box" style="margin-bottom: 16px; margin-top: 16px;">
<div class="conversation-search-box">
<input type="text" id="conversation-search-input" data-i18n="chat.searchHistory" data-i18n-attr="placeholder" placeholder="搜索历史记录..."
oninput="handleConversationSearch(this.value)"
onkeypress="if(event.key === 'Enter') handleConversationSearch(this.value)" />
@@ -790,6 +790,15 @@
</svg>
</button>
</div>
<!-- 按项目筛选对话 -->
<div class="conversation-project-filter">
<label class="conversation-project-filter-label" for="conversation-project-filter" data-i18n="chat.filterByProject">按项目筛选</label>
<select id="conversation-project-filter" class="conversation-project-filter-native" onchange="onConversationProjectFilterChange(this.value)" data-i18n="chat.filterByProject" data-i18n-attr="title" title="按项目筛选">
<option value="" data-i18n="chat.filterAllProjects">全部项目</option>
<option value="__none__" data-i18n="chat.filterUnboundProjects">未绑定项目</option>
</select>
</div>
<!-- 对话分组 -->
<div class="conversation-groups-section">
@@ -3764,6 +3773,10 @@
<div class="modal-header">
<h2 id="batch-manage-title">管理对话记录·共<span id="batch-manage-count">0</span></h2>
<div class="batch-manage-header-actions">
<select id="batch-project-filter" class="conversation-project-filter-native" onchange="applyBatchConversationFilters()" data-i18n="batchManageModal.filterByProject" data-i18n-attr="title" title="按项目筛选">
<option value="" data-i18n="chat.filterAllProjects">全部项目</option>
<option value="__none__" data-i18n="chat.filterUnboundProjects">未绑定项目</option>
</select>
<div class="batch-search-box">
<input type="text" id="batch-search-input" data-i18n="batchManageModal.searchPlaceholder" data-i18n-attr="placeholder" placeholder="搜索历史记录" oninput="filterBatchConversations(this.value)" />
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -3781,6 +3794,7 @@
<input type="checkbox" id="batch-select-all" onchange="toggleSelectAllBatch()" data-i18n="batchManageModal.selectAll" data-i18n-attr="title" title="全选" />
</div>
<div class="batch-table-col-name" data-i18n="batchManageModal.conversationName">对话名称</div>
<div class="batch-table-col-project" data-i18n="batchManageModal.project">项目</div>
<div class="batch-table-col-time" data-i18n="batchManageModal.lastTime">最近一次对话时间</div>
<div class="batch-table-col-action" data-i18n="batchManageModal.action">操作</div>
</div>