mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-03-31 00:09:29 +02:00
Add files via upload
This commit is contained in:
@@ -138,6 +138,7 @@
|
||||
"deleteGroupConfirm": "Are you sure you want to delete this group? Conversations in the group will not be deleted, but will be removed from the group.",
|
||||
"deleteConversationConfirm": "Are you sure you want to delete this conversation?",
|
||||
"renameFailed": "Rename failed",
|
||||
"downloadConversationFailed": "Failed to download conversation",
|
||||
"viewAttackChainSelectConv": "Please select a conversation to view attack chain",
|
||||
"viewAttackChainCurrentConv": "View attack chain of current conversation",
|
||||
"executeFailed": "Execution failed",
|
||||
@@ -1450,6 +1451,9 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"viewAttackChain": "View attack chain",
|
||||
"downloadMarkdown": "Download Markdown",
|
||||
"downloadMarkdownSummary": "Summary",
|
||||
"downloadMarkdownFull": "Full",
|
||||
"rename": "Rename",
|
||||
"pinConversation": "Pin conversation",
|
||||
"unpinConversation": "Unpin",
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
"deleteGroupConfirm": "确定要删除此分组吗?分组中的对话不会被删除,但会从分组中移除。",
|
||||
"deleteConversationConfirm": "确定要删除此对话吗?",
|
||||
"renameFailed": "重命名失败",
|
||||
"downloadConversationFailed": "下载对话失败",
|
||||
"viewAttackChainSelectConv": "请选择一个对话以查看攻击链",
|
||||
"viewAttackChainCurrentConv": "查看当前对话的攻击链",
|
||||
"executeFailed": "执行失败",
|
||||
@@ -1450,6 +1451,9 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"viewAttackChain": "查看攻击链",
|
||||
"downloadMarkdown": "下载 Markdown",
|
||||
"downloadMarkdownSummary": "简版",
|
||||
"downloadMarkdownFull": "完整版",
|
||||
"rename": "重命名",
|
||||
"pinConversation": "置顶此对话",
|
||||
"unpinConversation": "取消置顶",
|
||||
|
||||
@@ -4404,9 +4404,14 @@ async function showConversationContextMenu(event) {
|
||||
submenu.style.display = 'none';
|
||||
submenuVisible = false;
|
||||
}
|
||||
const downloadSubmenu = document.getElementById('download-markdown-submenu');
|
||||
if (downloadSubmenu) {
|
||||
downloadSubmenu.style.display = 'none';
|
||||
}
|
||||
// 清除所有定时器
|
||||
clearSubmenuHideTimeout();
|
||||
clearSubmenuShowTimeout();
|
||||
clearDownloadMarkdownSubmenuHideTimeout();
|
||||
submenuLoading = false;
|
||||
|
||||
const convId = contextMenuConversationId;
|
||||
@@ -4537,26 +4542,44 @@ async function showConversationContextMenu(event) {
|
||||
menu.style.top = top + 'px';
|
||||
|
||||
// 如果菜单在右侧,子菜单应该在左侧显示
|
||||
if (submenu && left < event.clientX) {
|
||||
submenu.style.left = 'auto';
|
||||
submenu.style.right = '100%';
|
||||
submenu.style.marginLeft = '0';
|
||||
submenu.style.marginRight = '4px';
|
||||
} else if (submenu) {
|
||||
submenu.style.left = '100%';
|
||||
submenu.style.right = 'auto';
|
||||
submenu.style.marginLeft = '4px';
|
||||
submenu.style.marginRight = '0';
|
||||
if (left < event.clientX) {
|
||||
if (submenu) {
|
||||
submenu.style.left = 'auto';
|
||||
submenu.style.right = '100%';
|
||||
submenu.style.marginLeft = '0';
|
||||
submenu.style.marginRight = '4px';
|
||||
}
|
||||
if (downloadSubmenu) {
|
||||
downloadSubmenu.style.left = 'auto';
|
||||
downloadSubmenu.style.right = '100%';
|
||||
downloadSubmenu.style.marginLeft = '0';
|
||||
downloadSubmenu.style.marginRight = '4px';
|
||||
}
|
||||
} else {
|
||||
if (submenu) {
|
||||
submenu.style.left = '100%';
|
||||
submenu.style.right = 'auto';
|
||||
submenu.style.marginLeft = '4px';
|
||||
submenu.style.marginRight = '0';
|
||||
}
|
||||
if (downloadSubmenu) {
|
||||
downloadSubmenu.style.left = '100%';
|
||||
downloadSubmenu.style.right = 'auto';
|
||||
downloadSubmenu.style.marginLeft = '4px';
|
||||
downloadSubmenu.style.marginRight = '0';
|
||||
}
|
||||
}
|
||||
|
||||
// 点击外部关闭菜单
|
||||
const closeMenu = (e) => {
|
||||
// 检查点击是否在主菜单或子菜单内
|
||||
const moveToGroupSubmenuEl = document.getElementById('move-to-group-submenu');
|
||||
const downloadMarkdownSubmenuEl = document.getElementById('download-markdown-submenu');
|
||||
const clickedInMenu = menu.contains(e.target);
|
||||
const clickedInSubmenu = moveToGroupSubmenuEl && moveToGroupSubmenuEl.contains(e.target);
|
||||
const clickedInDownloadSubmenu = downloadMarkdownSubmenuEl && downloadMarkdownSubmenuEl.contains(e.target);
|
||||
|
||||
if (!clickedInMenu && !clickedInSubmenu) {
|
||||
if (!clickedInMenu && !clickedInSubmenu && !clickedInDownloadSubmenu) {
|
||||
// 使用 closeContextMenu 确保同时关闭主菜单和子菜单
|
||||
closeContextMenu();
|
||||
document.removeEventListener('click', closeMenu);
|
||||
@@ -4950,6 +4973,8 @@ let submenuShowTimeout = null;
|
||||
let submenuLoading = false;
|
||||
// 子菜单是否已显示
|
||||
let submenuVisible = false;
|
||||
// 下载Markdown子菜单隐藏定时器
|
||||
let downloadMarkdownSubmenuHideTimeout = null;
|
||||
|
||||
// 隐藏移动到分组子菜单
|
||||
function hideMoveToGroupSubmenu() {
|
||||
@@ -4976,6 +5001,45 @@ function clearSubmenuShowTimeout() {
|
||||
}
|
||||
}
|
||||
|
||||
function clearDownloadMarkdownSubmenuHideTimeout() {
|
||||
if (downloadMarkdownSubmenuHideTimeout) {
|
||||
clearTimeout(downloadMarkdownSubmenuHideTimeout);
|
||||
downloadMarkdownSubmenuHideTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
function showDownloadMarkdownSubmenu() {
|
||||
const submenu = document.getElementById('download-markdown-submenu');
|
||||
if (!submenu) return;
|
||||
clearDownloadMarkdownSubmenuHideTimeout();
|
||||
submenu.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideDownloadMarkdownSubmenu() {
|
||||
const submenu = document.getElementById('download-markdown-submenu');
|
||||
if (!submenu) return;
|
||||
submenu.style.display = 'none';
|
||||
}
|
||||
|
||||
function handleDownloadMarkdownSubmenuEnter() {
|
||||
clearDownloadMarkdownSubmenuHideTimeout();
|
||||
showDownloadMarkdownSubmenu();
|
||||
}
|
||||
|
||||
function handleDownloadMarkdownSubmenuLeave(event) {
|
||||
const submenu = document.getElementById('download-markdown-submenu');
|
||||
if (!submenu) return;
|
||||
const relatedTarget = event.relatedTarget;
|
||||
if (relatedTarget && submenu.contains(relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
clearDownloadMarkdownSubmenuHideTimeout();
|
||||
downloadMarkdownSubmenuHideTimeout = setTimeout(() => {
|
||||
hideDownloadMarkdownSubmenu();
|
||||
downloadMarkdownSubmenuHideTimeout = null;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// 处理鼠标进入"移动到分组"菜单项(带防抖)
|
||||
function handleMoveToGroupSubmenuEnter() {
|
||||
// 清除隐藏定时器
|
||||
@@ -5178,6 +5242,146 @@ function showAttackChainFromContext() {
|
||||
showAttackChain(convId);
|
||||
}
|
||||
|
||||
function formatConversationDateForMarkdown(value) {
|
||||
if (!value) return '';
|
||||
const d = new Date(value);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
|
||||
return d.toLocaleString(locale, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
}
|
||||
|
||||
function getConversationRoleLabel(role) {
|
||||
switch (role) {
|
||||
case 'assistant':
|
||||
return 'Assistant';
|
||||
case 'user':
|
||||
return 'User';
|
||||
case 'system':
|
||||
return 'System';
|
||||
default:
|
||||
return role || 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function formatConversationAsMarkdown(conversation, options = {}) {
|
||||
const includeToolDetails = !!options.includeToolDetails;
|
||||
const title = (conversation && conversation.title ? String(conversation.title) : '').trim() || 'Untitled Conversation';
|
||||
const createdAt = formatConversationDateForMarkdown(conversation && conversation.createdAt);
|
||||
const updatedAt = formatConversationDateForMarkdown(conversation && conversation.updatedAt);
|
||||
const messages = Array.isArray(conversation && conversation.messages) ? conversation.messages : [];
|
||||
|
||||
let markdown = `# ${title}\n\n`;
|
||||
markdown += `- Conversation ID: \`${conversation && conversation.id ? conversation.id : ''}\`\n`;
|
||||
if (createdAt) markdown += `- Created At: ${createdAt}\n`;
|
||||
if (updatedAt) markdown += `- Updated At: ${updatedAt}\n`;
|
||||
markdown += `- Message Count: ${messages.length}\n\n`;
|
||||
markdown += '---\n\n';
|
||||
|
||||
if (messages.length === 0) {
|
||||
markdown += '_No messages in this conversation._\n';
|
||||
return markdown;
|
||||
}
|
||||
|
||||
messages.forEach((msg, index) => {
|
||||
const role = getConversationRoleLabel(msg && msg.role);
|
||||
const timestamp = formatConversationDateForMarkdown(msg && msg.createdAt);
|
||||
const content = msg && typeof msg.content === 'string' ? msg.content : '';
|
||||
|
||||
markdown += `## ${index + 1}. ${role}`;
|
||||
if (timestamp) markdown += ` (${timestamp})`;
|
||||
markdown += '\n\n';
|
||||
markdown += content ? `${content}\n\n` : '_[Empty message]_\n\n';
|
||||
|
||||
if (Array.isArray(msg && msg.processDetails) && msg.processDetails.length > 0) {
|
||||
markdown += '### Process Details\n\n';
|
||||
msg.processDetails.forEach((detail) => {
|
||||
const detailTime = formatConversationDateForMarkdown(detail && detail.timestamp);
|
||||
const eventType = detail && detail.eventType ? detail.eventType : 'event';
|
||||
const detailMsg = detail && detail.message ? detail.message : '';
|
||||
// Avoid "[label]:" pattern because some Markdown parsers treat it as link reference definition.
|
||||
markdown += `- \`${eventType}\``;
|
||||
if (detailTime) markdown += ` ${detailTime}`;
|
||||
if (detailMsg) markdown += `: ${detailMsg}`;
|
||||
markdown += '\n';
|
||||
|
||||
if (includeToolDetails && detail && detail.data && (eventType === 'tool_call' || eventType === 'tool_result')) {
|
||||
const pretty = JSON.stringify(detail.data, null, 2);
|
||||
markdown += '\n```json\n';
|
||||
markdown += pretty || '{}';
|
||||
markdown += '\n```\n';
|
||||
}
|
||||
});
|
||||
markdown += '\n';
|
||||
}
|
||||
|
||||
if (Array.isArray(msg && msg.mcpExecutionIds) && msg.mcpExecutionIds.length > 0) {
|
||||
markdown += `- MCP Execution IDs: ${msg.mcpExecutionIds.join(', ')}\n\n`;
|
||||
}
|
||||
|
||||
markdown += '---\n\n';
|
||||
});
|
||||
|
||||
return markdown;
|
||||
}
|
||||
|
||||
function buildConversationMarkdownFileName(conversation, options = {}) {
|
||||
const includeToolDetails = !!options.includeToolDetails;
|
||||
const title = (conversation && conversation.title ? String(conversation.title) : '').trim() || 'conversation';
|
||||
const safeTitle = title
|
||||
.replace(/[\\/:*?"<>|]/g, '_')
|
||||
.replace(/\s+/g, '_')
|
||||
.slice(0, 60) || 'conversation';
|
||||
const idPart = (conversation && conversation.id ? String(conversation.id) : '').slice(0, 8) || 'export';
|
||||
const modePart = includeToolDetails ? 'full' : 'summary';
|
||||
return `${safeTitle}_${idPart}_${modePart}.md`;
|
||||
}
|
||||
|
||||
// 从上下文菜单下载对话 Markdown
|
||||
async function downloadConversationMarkdownFromContext(includeToolDetails = false) {
|
||||
const convId = contextMenuConversationId;
|
||||
if (!convId) return;
|
||||
|
||||
try {
|
||||
const response = await apiFetch(`/api/conversations/${convId}`);
|
||||
let conversation = null;
|
||||
try {
|
||||
conversation = await response.json();
|
||||
} catch (e) {
|
||||
conversation = null;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const errorMsg = conversation && conversation.error ? conversation.error : 'unknown error';
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const markdown = formatConversationAsMarkdown(conversation || {}, { includeToolDetails });
|
||||
const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = buildConversationMarkdownFileName(conversation || {}, { includeToolDetails });
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('下载对话 Markdown 失败:', error);
|
||||
const failedLabel = typeof window.t === 'function' ? window.t('chat.downloadConversationFailed') : '下载失败';
|
||||
const errMsg = error && error.message ? error.message : 'unknown error';
|
||||
alert(failedLabel + ': ' + errMsg);
|
||||
}
|
||||
|
||||
closeContextMenu();
|
||||
}
|
||||
|
||||
// 从上下文菜单删除对话
|
||||
function deleteConversationFromContext() {
|
||||
const convId = contextMenuConversationId;
|
||||
@@ -5201,9 +5405,14 @@ function closeContextMenu() {
|
||||
submenu.style.display = 'none';
|
||||
submenuVisible = false;
|
||||
}
|
||||
const downloadSubmenu = document.getElementById('download-markdown-submenu');
|
||||
if (downloadSubmenu) {
|
||||
downloadSubmenu.style.display = 'none';
|
||||
}
|
||||
// 清除所有定时器
|
||||
clearSubmenuHideTimeout();
|
||||
clearSubmenuShowTimeout();
|
||||
clearDownloadMarkdownSubmenuHideTimeout();
|
||||
submenuLoading = false;
|
||||
contextMenuConversationId = null;
|
||||
}
|
||||
|
||||
@@ -2205,6 +2205,24 @@ version: 1.0.0<br>
|
||||
</svg>
|
||||
<span data-i18n="contextMenu.viewAttackChain">查看攻击链</span>
|
||||
</div>
|
||||
<div class="context-menu-item context-menu-item-has-submenu" onmouseenter="handleDownloadMarkdownSubmenuEnter()" onmouseleave="handleDownloadMarkdownSubmenuLeave(event)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 3v12m0 0l-4-4m4 4l4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span data-i18n="contextMenu.downloadMarkdown">下载 Markdown</span>
|
||||
<svg class="submenu-arrow" width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<div id="download-markdown-submenu" class="context-submenu" style="display: none;" onmouseenter="clearDownloadMarkdownSubmenuHideTimeout()" onmouseleave="hideDownloadMarkdownSubmenu()">
|
||||
<div class="context-submenu-item" onclick="downloadConversationMarkdownFromContext(false)">
|
||||
<span data-i18n="contextMenu.downloadMarkdownSummary">简版</span>
|
||||
</div>
|
||||
<div class="context-submenu-item" onclick="downloadConversationMarkdownFromContext(true)">
|
||||
<span data-i18n="contextMenu.downloadMarkdownFull">完整版</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="context-menu-divider"></div>
|
||||
<div class="context-menu-item" onclick="renameConversation()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
Reference in New Issue
Block a user