mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-20 18:08:05 +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.",
|
"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?",
|
"deleteConversationConfirm": "Are you sure you want to delete this conversation?",
|
||||||
"renameFailed": "Rename failed",
|
"renameFailed": "Rename failed",
|
||||||
|
"downloadConversationFailed": "Failed to download conversation",
|
||||||
"viewAttackChainSelectConv": "Please select a conversation to view attack chain",
|
"viewAttackChainSelectConv": "Please select a conversation to view attack chain",
|
||||||
"viewAttackChainCurrentConv": "View attack chain of current conversation",
|
"viewAttackChainCurrentConv": "View attack chain of current conversation",
|
||||||
"executeFailed": "Execution failed",
|
"executeFailed": "Execution failed",
|
||||||
@@ -1450,6 +1451,9 @@
|
|||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"viewAttackChain": "View attack chain",
|
"viewAttackChain": "View attack chain",
|
||||||
|
"downloadMarkdown": "Download Markdown",
|
||||||
|
"downloadMarkdownSummary": "Summary",
|
||||||
|
"downloadMarkdownFull": "Full",
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"pinConversation": "Pin conversation",
|
"pinConversation": "Pin conversation",
|
||||||
"unpinConversation": "Unpin",
|
"unpinConversation": "Unpin",
|
||||||
|
|||||||
@@ -138,6 +138,7 @@
|
|||||||
"deleteGroupConfirm": "确定要删除此分组吗?分组中的对话不会被删除,但会从分组中移除。",
|
"deleteGroupConfirm": "确定要删除此分组吗?分组中的对话不会被删除,但会从分组中移除。",
|
||||||
"deleteConversationConfirm": "确定要删除此对话吗?",
|
"deleteConversationConfirm": "确定要删除此对话吗?",
|
||||||
"renameFailed": "重命名失败",
|
"renameFailed": "重命名失败",
|
||||||
|
"downloadConversationFailed": "下载对话失败",
|
||||||
"viewAttackChainSelectConv": "请选择一个对话以查看攻击链",
|
"viewAttackChainSelectConv": "请选择一个对话以查看攻击链",
|
||||||
"viewAttackChainCurrentConv": "查看当前对话的攻击链",
|
"viewAttackChainCurrentConv": "查看当前对话的攻击链",
|
||||||
"executeFailed": "执行失败",
|
"executeFailed": "执行失败",
|
||||||
@@ -1450,6 +1451,9 @@
|
|||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"viewAttackChain": "查看攻击链",
|
"viewAttackChain": "查看攻击链",
|
||||||
|
"downloadMarkdown": "下载 Markdown",
|
||||||
|
"downloadMarkdownSummary": "简版",
|
||||||
|
"downloadMarkdownFull": "完整版",
|
||||||
"rename": "重命名",
|
"rename": "重命名",
|
||||||
"pinConversation": "置顶此对话",
|
"pinConversation": "置顶此对话",
|
||||||
"unpinConversation": "取消置顶",
|
"unpinConversation": "取消置顶",
|
||||||
|
|||||||
+220
-11
@@ -4404,9 +4404,14 @@ async function showConversationContextMenu(event) {
|
|||||||
submenu.style.display = 'none';
|
submenu.style.display = 'none';
|
||||||
submenuVisible = false;
|
submenuVisible = false;
|
||||||
}
|
}
|
||||||
|
const downloadSubmenu = document.getElementById('download-markdown-submenu');
|
||||||
|
if (downloadSubmenu) {
|
||||||
|
downloadSubmenu.style.display = 'none';
|
||||||
|
}
|
||||||
// 清除所有定时器
|
// 清除所有定时器
|
||||||
clearSubmenuHideTimeout();
|
clearSubmenuHideTimeout();
|
||||||
clearSubmenuShowTimeout();
|
clearSubmenuShowTimeout();
|
||||||
|
clearDownloadMarkdownSubmenuHideTimeout();
|
||||||
submenuLoading = false;
|
submenuLoading = false;
|
||||||
|
|
||||||
const convId = contextMenuConversationId;
|
const convId = contextMenuConversationId;
|
||||||
@@ -4537,26 +4542,44 @@ async function showConversationContextMenu(event) {
|
|||||||
menu.style.top = top + 'px';
|
menu.style.top = top + 'px';
|
||||||
|
|
||||||
// 如果菜单在右侧,子菜单应该在左侧显示
|
// 如果菜单在右侧,子菜单应该在左侧显示
|
||||||
if (submenu && left < event.clientX) {
|
if (left < event.clientX) {
|
||||||
submenu.style.left = 'auto';
|
if (submenu) {
|
||||||
submenu.style.right = '100%';
|
submenu.style.left = 'auto';
|
||||||
submenu.style.marginLeft = '0';
|
submenu.style.right = '100%';
|
||||||
submenu.style.marginRight = '4px';
|
submenu.style.marginLeft = '0';
|
||||||
} else if (submenu) {
|
submenu.style.marginRight = '4px';
|
||||||
submenu.style.left = '100%';
|
}
|
||||||
submenu.style.right = 'auto';
|
if (downloadSubmenu) {
|
||||||
submenu.style.marginLeft = '4px';
|
downloadSubmenu.style.left = 'auto';
|
||||||
submenu.style.marginRight = '0';
|
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 closeMenu = (e) => {
|
||||||
// 检查点击是否在主菜单或子菜单内
|
// 检查点击是否在主菜单或子菜单内
|
||||||
const moveToGroupSubmenuEl = document.getElementById('move-to-group-submenu');
|
const moveToGroupSubmenuEl = document.getElementById('move-to-group-submenu');
|
||||||
|
const downloadMarkdownSubmenuEl = document.getElementById('download-markdown-submenu');
|
||||||
const clickedInMenu = menu.contains(e.target);
|
const clickedInMenu = menu.contains(e.target);
|
||||||
const clickedInSubmenu = moveToGroupSubmenuEl && moveToGroupSubmenuEl.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 确保同时关闭主菜单和子菜单
|
||||||
closeContextMenu();
|
closeContextMenu();
|
||||||
document.removeEventListener('click', closeMenu);
|
document.removeEventListener('click', closeMenu);
|
||||||
@@ -4950,6 +4973,8 @@ let submenuShowTimeout = null;
|
|||||||
let submenuLoading = false;
|
let submenuLoading = false;
|
||||||
// 子菜单是否已显示
|
// 子菜单是否已显示
|
||||||
let submenuVisible = false;
|
let submenuVisible = false;
|
||||||
|
// 下载Markdown子菜单隐藏定时器
|
||||||
|
let downloadMarkdownSubmenuHideTimeout = null;
|
||||||
|
|
||||||
// 隐藏移动到分组子菜单
|
// 隐藏移动到分组子菜单
|
||||||
function hideMoveToGroupSubmenu() {
|
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() {
|
function handleMoveToGroupSubmenuEnter() {
|
||||||
// 清除隐藏定时器
|
// 清除隐藏定时器
|
||||||
@@ -5178,6 +5242,146 @@ function showAttackChainFromContext() {
|
|||||||
showAttackChain(convId);
|
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() {
|
function deleteConversationFromContext() {
|
||||||
const convId = contextMenuConversationId;
|
const convId = contextMenuConversationId;
|
||||||
@@ -5201,9 +5405,14 @@ function closeContextMenu() {
|
|||||||
submenu.style.display = 'none';
|
submenu.style.display = 'none';
|
||||||
submenuVisible = false;
|
submenuVisible = false;
|
||||||
}
|
}
|
||||||
|
const downloadSubmenu = document.getElementById('download-markdown-submenu');
|
||||||
|
if (downloadSubmenu) {
|
||||||
|
downloadSubmenu.style.display = 'none';
|
||||||
|
}
|
||||||
// 清除所有定时器
|
// 清除所有定时器
|
||||||
clearSubmenuHideTimeout();
|
clearSubmenuHideTimeout();
|
||||||
clearSubmenuShowTimeout();
|
clearSubmenuShowTimeout();
|
||||||
|
clearDownloadMarkdownSubmenuHideTimeout();
|
||||||
submenuLoading = false;
|
submenuLoading = false;
|
||||||
contextMenuConversationId = null;
|
contextMenuConversationId = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2205,6 +2205,24 @@ version: 1.0.0<br>
|
|||||||
</svg>
|
</svg>
|
||||||
<span data-i18n="contextMenu.viewAttackChain">查看攻击链</span>
|
<span data-i18n="contextMenu.viewAttackChain">查看攻击链</span>
|
||||||
</div>
|
</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-divider"></div>
|
||||||
<div class="context-menu-item" onclick="renameConversation()">
|
<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">
|
<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