mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-14 20:48:10 +02:00
Add files via upload
This commit is contained in:
@@ -3593,6 +3593,11 @@ header {
|
||||
background: rgba(255, 112, 67, 0.12);
|
||||
}
|
||||
|
||||
.timeline-item-user_interrupt_continue {
|
||||
border-left-color: #d97706;
|
||||
background: rgba(217, 119, 6, 0.08);
|
||||
}
|
||||
|
||||
.timeline-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -288,6 +288,7 @@
|
||||
"error": "Error",
|
||||
"streamNetworkErrorHint": "Connection lost ({{detail}}). A long task may still be running on the server; check running tasks at the top or refresh this conversation later.",
|
||||
"taskCancelled": "Task cancelled",
|
||||
"userInterruptContinueTitle": "⏸️ User interrupt & continue",
|
||||
"unknownTool": "Unknown tool",
|
||||
"einoAgentReplyTitle": "Sub-agent reply",
|
||||
"einoStreamErrorTitle": "⚠️ Eino stream interrupted ({{agent}})",
|
||||
@@ -396,7 +397,7 @@
|
||||
"stopTask": "Stop task",
|
||||
"interruptModalTitle": "Interrupt current step",
|
||||
"interruptReasonLabel": "Interrupt note",
|
||||
"interruptModalHint": "Same as MCP monitor \"Stop tool\": ends only the in-flight tool call; the conversation and this run continue. Optional note is merged into the tool result (bilingual USER INTERRUPT NOTE, not raw CLI). Leave empty for a plain stop. If no tool is running yet (model still thinking), wait for a tool call or use \"Stop completely\".",
|
||||
"interruptModalHint": "When a tool is running: same as MCP monitor \"Stop tool\" — only that call is stopped and the run continues; your note can be merged into the tool result (USER INTERRUPT NOTE). When no tool is running (model thinking/streaming only): \"Interrupt & continue\" still works — current output pauses, your note is merged into context and the run resumes automatically; the progress timeline shows a \"User interrupt & continue\" entry. Use this instead of a full stop when you only want to steer; use \"Stop completely\" to end the whole task.",
|
||||
"interruptReasonPlaceholder": "e.g. Tool is too slow—skip and summarize…",
|
||||
"interruptReasonRequired": "Please enter a short note so the model can continue accordingly.",
|
||||
"interruptSubmitting": "Submitting...",
|
||||
|
||||
@@ -277,6 +277,7 @@
|
||||
"error": "错误",
|
||||
"streamNetworkErrorHint": "连接已中断({{detail}})。长时间任务可能仍在后端执行,请查看顶部「运行中」任务或稍后刷新本对话。",
|
||||
"taskCancelled": "任务已取消",
|
||||
"userInterruptContinueTitle": "⏸️ 用户中断并继续",
|
||||
"unknownTool": "未知工具",
|
||||
"einoAgentReplyTitle": "子代理回复",
|
||||
"einoStreamErrorTitle": "⚠️ Eino 流式中断({{agent}})",
|
||||
@@ -385,7 +386,7 @@
|
||||
"stopTask": "停止任务",
|
||||
"interruptModalTitle": "中断当前步骤",
|
||||
"interruptReasonLabel": "中断说明",
|
||||
"interruptModalHint": "与 MCP 监控页「终止工具」一致:仅结束当前这一次工具调用,整条对话与本轮推理会继续;工具返回中可附带说明(中英 USER INTERRUPT NOTE 块,与命令行原文区分)。留空则等同仅终止工具。若当前没有工具在执行(模型尚在思考),请等待工具开始或改用「彻底停止」。",
|
||||
"interruptModalHint": "有工具在执行时:与 MCP 监控页「终止工具」一致,仅结束当前这一次工具调用,本轮推理会继续;说明可写入工具返回(USER INTERRUPT NOTE)。无工具在执行时(模型纯思考/流式输出):仍可「中断并继续」——会暂停当前输出,把你的说明合并进上下文并自动续跑;进度详情时间线会出现「用户中断并继续」条目。不需要整轮停止时请优先用本按钮;要结束整条任务请用「彻底停止」。",
|
||||
"interruptReasonPlaceholder": "例如:工具耗时过长,请先跳过并总结当前结果…",
|
||||
"interruptReasonRequired": "请填写中断说明,以便模型根据你的意图继续。",
|
||||
"interruptSubmitting": "提交中...",
|
||||
|
||||
@@ -26,6 +26,11 @@ const DRAFT_SAVE_DELAY = 500; // 500ms防抖延迟
|
||||
// 对话文件上传相关(后端会拼接路径与内容发给大模型,前端不再重复发文件列表)
|
||||
const MAX_CHAT_FILES = 10;
|
||||
const CHAT_FILE_DEFAULT_PROMPT = '请根据上传的文件内容进行分析。';
|
||||
/** 与 handler.formatInterruptContinueUserMessage 首段一致;主对话不展示,仅迭代详情(user_interrupt_continue) */
|
||||
const CHAT_INTERRUPT_CONTINUE_USER_PREFIX = '【用户补充 / 中断后继续】';
|
||||
function isInterruptContinueInjectChatMessage(content) {
|
||||
return typeof content === 'string' && content.trimStart().startsWith(CHAT_INTERRUPT_CONTINUE_USER_PREFIX);
|
||||
}
|
||||
/**
|
||||
* 对话附件:选文件后异步 POST /api/chat-uploads,发送时只传 serverPath(绝对路径),请求体不再内联大文件内容。
|
||||
* @type {{ id: number, fileName: string, mimeType: string, serverPath: string|null, uploading: boolean, uploadPercent: number, uploadPromise: Promise<void>|null, uploadError: string|null }[]}
|
||||
@@ -2259,6 +2264,10 @@ function renderProcessDetails(messageId, processDetails) {
|
||||
itemTitle = agPx + '🧑⚖️ HITL · ' + hitlMsg;
|
||||
} else if (eventType === 'progress') {
|
||||
itemTitle = typeof window.translateProgressMessage === 'function' ? window.translateProgressMessage(detail.message || '') : (detail.message || '');
|
||||
} else if (eventType === 'user_interrupt_continue') {
|
||||
itemTitle = typeof window.t === 'function'
|
||||
? window.t('chat.userInterruptContinueTitle')
|
||||
: '⏸️ 用户中断并继续';
|
||||
}
|
||||
|
||||
addTimelineItem(timeline, eventType, {
|
||||
@@ -2975,6 +2984,9 @@ async function loadConversation(conversationId) {
|
||||
|
||||
// 渲染单条消息的辅助函数
|
||||
const renderOneMessage = (msg) => {
|
||||
if (msg.role === 'user' && isInterruptContinueInjectChatMessage(msg.content)) {
|
||||
return;
|
||||
}
|
||||
let displayContent = msg.content;
|
||||
if (msg.role === 'assistant' && msg.content === '处理中...' && msg.processDetails && msg.processDetails.length > 0) {
|
||||
for (let i = msg.processDetails.length - 1; i >= 0; i--) {
|
||||
@@ -6639,6 +6651,9 @@ function formatConversationAsMarkdown(conversation, options = {}) {
|
||||
}
|
||||
|
||||
messages.forEach((msg, index) => {
|
||||
if (msg && msg.role === 'user' && isInterruptContinueInjectChatMessage(msg.content)) {
|
||||
return;
|
||||
}
|
||||
const role = getConversationRoleLabel(msg && msg.role);
|
||||
const timestamp = formatConversationDateForMarkdown(msg && msg.createdAt);
|
||||
const content = msg && typeof msg.content === 'string' ? msg.content : '';
|
||||
|
||||
+64
-12
@@ -784,19 +784,33 @@ function integrateProgressToMCPSection(progressId, assistantMessageId, mcpExecut
|
||||
mcpSection.appendChild(buttonsContainer);
|
||||
}
|
||||
|
||||
const hasExecBtns = buttonsContainer.querySelector('.mcp-detail-btn:not(.process-detail-btn)');
|
||||
if (mcpIds.length > 0 && !hasExecBtns) {
|
||||
mcpIds.forEach((execId, index) => {
|
||||
let maxExecIndex = 0;
|
||||
const existingExecBtns = buttonsContainer.querySelectorAll('.mcp-detail-btn:not(.process-detail-btn)');
|
||||
existingExecBtns.forEach(function (btn) {
|
||||
const n = parseInt(btn.dataset.execIndex, 10);
|
||||
if (!isNaN(n) && n > maxExecIndex) maxExecIndex = n;
|
||||
});
|
||||
const seenExec = new Set();
|
||||
existingExecBtns.forEach(function (btn) {
|
||||
if (btn.dataset.execId) seenExec.add(String(btn.dataset.execId).trim());
|
||||
});
|
||||
let appendedAny = false;
|
||||
if (mcpIds.length > 0) {
|
||||
mcpIds.forEach(function (execId) {
|
||||
const id = execId != null ? String(execId).trim() : '';
|
||||
if (!id || seenExec.has(id)) return;
|
||||
seenExec.add(id);
|
||||
maxExecIndex += 1;
|
||||
appendedAny = true;
|
||||
const detailBtn = document.createElement('button');
|
||||
detailBtn.className = 'mcp-detail-btn';
|
||||
detailBtn.dataset.execId = execId;
|
||||
detailBtn.dataset.execIndex = String(index + 1);
|
||||
detailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.callNumber', { n: index + 1 }) : '调用 #' + (index + 1)) + '</span>';
|
||||
detailBtn.onclick = () => showMCPDetail(execId);
|
||||
detailBtn.dataset.execId = id;
|
||||
detailBtn.dataset.execIndex = String(maxExecIndex);
|
||||
detailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.callNumber', { n: maxExecIndex }) : '调用 #' + maxExecIndex) + '</span>';
|
||||
detailBtn.onclick = function () { showMCPDetail(id); };
|
||||
buttonsContainer.appendChild(detailBtn);
|
||||
});
|
||||
// 使用批量 API 一次性获取所有工具名称(消除 N 次单独请求)
|
||||
if (typeof batchUpdateButtonToolNames === 'function') {
|
||||
if (appendedAny && typeof batchUpdateButtonToolNames === 'function') {
|
||||
batchUpdateButtonToolNames(buttonsContainer, mcpIds);
|
||||
}
|
||||
}
|
||||
@@ -1079,6 +1093,24 @@ function resolveStreamTimeline(progressId) {
|
||||
return timeline;
|
||||
}
|
||||
|
||||
/** 去重合并 MCP execution id(顺序:先 prev 后 next),用于多段 Run / 多次 SSE 同一任务。 */
|
||||
function mergeMcpExecutionIDLists(prev, next) {
|
||||
const seen = new Set();
|
||||
const out = [];
|
||||
const add = function (arr) {
|
||||
if (!Array.isArray(arr)) return;
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const s = arr[i] != null ? String(arr[i]).trim() : '';
|
||||
if (!s || seen.has(s)) continue;
|
||||
seen.add(s);
|
||||
out.push(s);
|
||||
}
|
||||
};
|
||||
add(prev);
|
||||
add(next);
|
||||
return out;
|
||||
}
|
||||
|
||||
// 处理流式事件
|
||||
function handleStreamEvent(event, progressElement, progressId,
|
||||
getAssistantId, setAssistantId, getMcpIds, setMcpIds) {
|
||||
@@ -1320,6 +1352,19 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'user_interrupt_continue': {
|
||||
const d = event.data || {};
|
||||
const titleBase = typeof window.t === 'function'
|
||||
? window.t('chat.userInterruptContinueTitle')
|
||||
: '⏸️ 用户中断并继续';
|
||||
addTimelineItem(timeline, 'user_interrupt_continue', {
|
||||
title: titleBase,
|
||||
message: event.message || '',
|
||||
data: d
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'eino_stream_error': {
|
||||
const d = event.data || {};
|
||||
const agent = d.einoAgent ? String(d.einoAgent) : '';
|
||||
@@ -1672,7 +1717,7 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
|
||||
const responseData = event.data || {};
|
||||
const mcpIds = responseData.mcpExecutionIds || [];
|
||||
setMcpIds(mcpIds);
|
||||
setMcpIds(mergeMcpExecutionIDLists(typeof getMcpIds === 'function' ? (getMcpIds() || []) : [], mcpIds));
|
||||
|
||||
if (responseData.conversationId) {
|
||||
// 如果用户已经开始了新对话(currentConversationId 为 null),且这个事件来自旧对话,则忽略
|
||||
@@ -1748,7 +1793,7 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
|
||||
// 先更新 mcp ids
|
||||
const responseData = event.data || {};
|
||||
const mcpIds = responseData.mcpExecutionIds || [];
|
||||
const mcpIds = mergeMcpExecutionIDLists(typeof getMcpIds === 'function' ? (getMcpIds() || []) : [], responseData.mcpExecutionIds || []);
|
||||
setMcpIds(mcpIds);
|
||||
|
||||
// 更新对话ID
|
||||
@@ -2272,7 +2317,7 @@ async function attachRunningTaskEventStream(conversationId) {
|
||||
if (line.indexOf('data: ') === 0) {
|
||||
try {
|
||||
const eventData = JSON.parse(line.slice(6));
|
||||
handleStreamEvent(eventData, null, progressId, getAssistantIdFn, setAssistantIdFn, function () { return mcpIds; }, function (ids) { mcpIds = ids; });
|
||||
handleStreamEvent(eventData, null, progressId, getAssistantIdFn, setAssistantIdFn, function () { return mcpIds; }, function (ids) { mcpIds = mergeMcpExecutionIDLists(mcpIds, ids || []); });
|
||||
} catch (e) {
|
||||
console.error('task-events parse', e);
|
||||
}
|
||||
@@ -2485,6 +2530,11 @@ function addTimelineItem(timeline, type, options) {
|
||||
${escapeHtml(options.message || taskCancelledLabel)}
|
||||
</div>
|
||||
`;
|
||||
} else if (type === 'user_interrupt_continue' && options.message) {
|
||||
const streamBody = typeof formatTimelineStreamBody === 'function'
|
||||
? formatTimelineStreamBody(options.message, options.data)
|
||||
: options.message;
|
||||
content += `<div class="timeline-item-content">${formatMarkdown(streamBody)}</div>`;
|
||||
}
|
||||
|
||||
item.innerHTML = content;
|
||||
@@ -3386,6 +3436,8 @@ function refreshProgressAndTimelineI18n() {
|
||||
titleSpan.textContent = ap + '\uD83D\uDCAC ' + _t('chat.einoAgentReplyTitle');
|
||||
} else if (type === 'cancelled') {
|
||||
titleSpan.textContent = '\u26D4 ' + _t('chat.taskCancelled');
|
||||
} else if (type === 'user_interrupt_continue') {
|
||||
titleSpan.textContent = _t('chat.userInterruptContinueTitle');
|
||||
} else if (type === 'progress' && item.dataset.progressMessage !== undefined) {
|
||||
titleSpan.textContent = typeof window.translateProgressMessage === 'function' ? window.translateProgressMessage(item.dataset.progressMessage) : item.dataset.progressMessage;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user