|
|
@@ -73,6 +73,15 @@
|
|
|
@reject="handleReject"
|
|
|
/>
|
|
|
|
|
|
+ <!-- 下载确认条 -->
|
|
|
+ <div v-if="downloadConfirm" class="download-confirm-bar">
|
|
|
+ <span class="download-confirm-text">是否下载 {{ downloadConfirm.filename }} 到本地?</span>
|
|
|
+ <div class="download-confirm-actions">
|
|
|
+ <button class="download-confirm-btn download-btn" @click="doDownload">下载</button>
|
|
|
+ <button class="download-confirm-btn cancel-btn" @click="downloadConfirm = null">取消</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<!-- 输入区域 -->
|
|
|
<ChatInputArea
|
|
|
ref="chatInputRef"
|
|
|
@@ -200,6 +209,7 @@
|
|
|
const editMode = ref('full');
|
|
|
const pendingApprovalSessionId = ref('');
|
|
|
const pendingApprovalThreadId = ref('');
|
|
|
+ const downloadConfirm = ref<{ url: string; filename: string } | null>(null);
|
|
|
const contextBreakdown = ref({ messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 });
|
|
|
|
|
|
const panelCollapsed = ref(false);
|
|
|
@@ -309,10 +319,6 @@
|
|
|
const newTask = taskList.value.find((t) => t.id === taskId);
|
|
|
if (!newTask) return;
|
|
|
|
|
|
- if (newTask.sessionId) {
|
|
|
- taskList.value = taskList.value.filter((t) => t.sessionId);
|
|
|
- }
|
|
|
-
|
|
|
inputMessage.value = '';
|
|
|
pendingFile.value = null;
|
|
|
previewFile.value = null;
|
|
|
@@ -374,6 +380,10 @@
|
|
|
};
|
|
|
|
|
|
const handlePinTask = async (task: Task) => {
|
|
|
+ if (!task.sessionId) {
|
|
|
+ message.warning('请先发送消息后再置顶');
|
|
|
+ return;
|
|
|
+ }
|
|
|
try {
|
|
|
await pinSession(task.sessionId);
|
|
|
await fetchSessionList();
|
|
|
@@ -395,11 +405,11 @@
|
|
|
};
|
|
|
|
|
|
const handleEditModeChange = async (modeKey: string) => {
|
|
|
+ editMode.value = modeKey;
|
|
|
const sessionId = getCurrentSessionId();
|
|
|
if (!sessionId) return;
|
|
|
try {
|
|
|
await putSessionMode(sessionId, modeKey);
|
|
|
- editMode.value = modeKey;
|
|
|
} catch {
|
|
|
message.error('切换模式失败');
|
|
|
}
|
|
|
@@ -409,8 +419,12 @@
|
|
|
const sessionId = pendingApprovalSessionId.value || getCurrentSessionId();
|
|
|
if (!sessionId) return;
|
|
|
|
|
|
- // 清除当前消息的 pendingApproval 状态,获取中断前累计时长和思考内容
|
|
|
- const lastMsg = messages.value[messages.value.length - 1];
|
|
|
+ const approveTask = taskList.value.find((t) => t.sessionId === sessionId);
|
|
|
+ if (!approveTask) return;
|
|
|
+
|
|
|
+ // 清除中断消息的 pendingApproval 状态,获取中断前累计时长和思考内容
|
|
|
+ const taskMessages = approveTask.messages;
|
|
|
+ const lastMsg = taskMessages[taskMessages.length - 1];
|
|
|
const prevElapsedMs = lastMsg?.pendingApprovalData?.elapsedMs || 0;
|
|
|
const prevThinkingContent = lastMsg?.pendingApprovalData?.prevThinkingContent || lastMsg?.thinkingContent || '';
|
|
|
if (lastMsg && lastMsg.isPendingApproval) {
|
|
|
@@ -418,7 +432,7 @@
|
|
|
}
|
|
|
|
|
|
const now = dayjs();
|
|
|
- const aiMsgIndex = messages.value.length;
|
|
|
+ const aiMsgIndex = taskMessages.length;
|
|
|
const aiMsg: Message = {
|
|
|
type: 'ai',
|
|
|
content: '',
|
|
|
@@ -430,10 +444,15 @@
|
|
|
generateStartTime: Date.now(),
|
|
|
baseDurationMs: prevElapsedMs,
|
|
|
};
|
|
|
- messages.value.push(aiMsg);
|
|
|
- await scrollToBottom();
|
|
|
+ taskMessages.push(aiMsg);
|
|
|
+ approveTask.messages = [...taskMessages];
|
|
|
+ if (currentTaskId.value === approveTask.id) {
|
|
|
+ messages.value = approveTask.messages;
|
|
|
+ await scrollToBottom();
|
|
|
+ }
|
|
|
|
|
|
loading.value = true;
|
|
|
+ approveTask.isStreaming = true;
|
|
|
|
|
|
try {
|
|
|
const result = await chatResumeStream(
|
|
|
@@ -443,8 +462,8 @@
|
|
|
action: 'approve',
|
|
|
},
|
|
|
(chunk: string) => {
|
|
|
- parseSSEData(chunk, aiMsgIndex);
|
|
|
- scrollToBottom();
|
|
|
+ parseSSEData(chunk, aiMsgIndex, approveTask.id);
|
|
|
+ if (currentTaskId.value === approveTask.id) scrollToBottom();
|
|
|
}
|
|
|
);
|
|
|
|
|
|
@@ -454,16 +473,18 @@
|
|
|
} catch (error) {
|
|
|
console.error('审批继续执行失败:', error);
|
|
|
message.error('审批失败,请重试');
|
|
|
- const errorMsg = messages.value[aiMsgIndex];
|
|
|
- if (errorMsg && errorMsg.isLoading) {
|
|
|
- errorMsg.isLoading = false;
|
|
|
- errorMsg.content = '抱歉,审批请求失败,请稍后重试。';
|
|
|
+ if (aiMsg.isLoading) {
|
|
|
+ aiMsg.isLoading = false;
|
|
|
+ aiMsg.content = '抱歉,审批请求失败,请稍后重试。';
|
|
|
}
|
|
|
} finally {
|
|
|
loading.value = false;
|
|
|
+ approveTask.isStreaming = false;
|
|
|
pendingApprovalSessionId.value = '';
|
|
|
pendingApprovalThreadId.value = '';
|
|
|
- await scrollToBottom();
|
|
|
+ if (currentTaskId.value === approveTask.id) {
|
|
|
+ await scrollToBottom();
|
|
|
+ }
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -471,8 +492,12 @@
|
|
|
const sessionId = pendingApprovalSessionId.value || getCurrentSessionId();
|
|
|
if (!sessionId) return;
|
|
|
|
|
|
- // 清除当前消息的 pendingApproval 状态
|
|
|
- const lastMsg = messages.value[messages.value.length - 1];
|
|
|
+ const rejectTask = taskList.value.find((t) => t.sessionId === sessionId);
|
|
|
+ if (!rejectTask) return;
|
|
|
+
|
|
|
+ // 清除中断消息的 pendingApproval 状态
|
|
|
+ const taskMessages = rejectTask.messages;
|
|
|
+ const lastMsg = taskMessages[taskMessages.length - 1];
|
|
|
if (lastMsg && lastMsg.isPendingApproval) {
|
|
|
lastMsg.isPendingApproval = false;
|
|
|
}
|
|
|
@@ -497,17 +522,16 @@
|
|
|
time: now.format('HH:mm'),
|
|
|
createdAt: now.toISOString(),
|
|
|
};
|
|
|
- messages.value.push(rejectMsg);
|
|
|
+ taskMessages.push(rejectMsg);
|
|
|
+ rejectTask.messages = [...taskMessages];
|
|
|
|
|
|
pendingApprovalSessionId.value = '';
|
|
|
pendingApprovalThreadId.value = '';
|
|
|
|
|
|
- const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
- if (currentTask) {
|
|
|
- currentTask.messages = [...messages.value];
|
|
|
+ if (currentTaskId.value === rejectTask.id) {
|
|
|
+ messages.value = rejectTask.messages;
|
|
|
+ await scrollToBottom();
|
|
|
}
|
|
|
-
|
|
|
- await scrollToBottom();
|
|
|
};
|
|
|
|
|
|
const handleDeleteTask = (task: Task, _index: number) => {
|
|
|
@@ -681,6 +705,17 @@
|
|
|
document.body.removeChild(a);
|
|
|
};
|
|
|
|
|
|
+ const doDownload = () => {
|
|
|
+ if (!downloadConfirm.value) return;
|
|
|
+ const a = document.createElement('a');
|
|
|
+ a.href = downloadConfirm.value.url;
|
|
|
+ a.download = downloadConfirm.value.filename;
|
|
|
+ document.body.appendChild(a);
|
|
|
+ a.click();
|
|
|
+ document.body.removeChild(a);
|
|
|
+ downloadConfirm.value = null;
|
|
|
+ };
|
|
|
+
|
|
|
const downloadWordFile = (url: string) => {
|
|
|
if (!url) return;
|
|
|
const a = document.createElement('a');
|
|
|
@@ -820,8 +855,17 @@
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const handleSSEMessage = (data: any, aiMsgIndex: number) => {
|
|
|
- const aiMsg = messages.value[aiMsgIndex];
|
|
|
+ const handleSSEMessage = (data: any, aiMsgIndex: number, taskId: string) => {
|
|
|
+ const streamingTask = taskList.value.find((t) => t.id === taskId);
|
|
|
+ if (!streamingTask) return;
|
|
|
+
|
|
|
+ // 当前正在查看流式任务时,新消息被 push 到 messages.value 而非 task.messages
|
|
|
+ // 所以需要从 messages.value 读取;否则从任务存储的 messages 读取
|
|
|
+ const isViewingTask = currentTaskId.value === taskId;
|
|
|
+ const sourceArr = isViewingTask ? messages.value : streamingTask.messages;
|
|
|
+ const aiMsg = sourceArr[aiMsgIndex];
|
|
|
+ if (!aiMsg) return;
|
|
|
+
|
|
|
const normalizedContent = normalizeLineBreaks(data.content || '');
|
|
|
|
|
|
// 初始化 thinkingSteps 数组
|
|
|
@@ -911,6 +955,9 @@
|
|
|
message: data.message || '执行完成',
|
|
|
timestamp: Date.now(),
|
|
|
});
|
|
|
+ if (data.download_url) {
|
|
|
+ downloadConfirm.value = { url: data.download_url, filename: data.filename || '文件' };
|
|
|
+ }
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
@@ -964,8 +1011,7 @@
|
|
|
case 'word_download':
|
|
|
if (normalizedContent) {
|
|
|
aiMsg.wordDownloadUrl = normalizedContent;
|
|
|
- const wordTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
- if (wordTask) wordTask.wordUrl = normalizedContent;
|
|
|
+ streamingTask.wordUrl = normalizedContent;
|
|
|
}
|
|
|
break;
|
|
|
|
|
|
@@ -991,27 +1037,30 @@
|
|
|
const elapsedMs = aiMsg.baseDurationMs || (aiMsg.generateStartTime ? Date.now() - aiMsg.generateStartTime : 0);
|
|
|
aiMsg.isLoading = false;
|
|
|
aiMsg.isPendingApproval = true;
|
|
|
+ const interruptSessionId = data.session_id || streamingTask.sessionId;
|
|
|
aiMsg.pendingApprovalData = {
|
|
|
message: data.message || '智能体制定了执行计划,等待您的审批...',
|
|
|
- sessionId: data.session_id || getCurrentSessionId(),
|
|
|
+ sessionId: interruptSessionId,
|
|
|
threadId: data.thread_id,
|
|
|
interrupts: data.interrupts,
|
|
|
elapsedMs,
|
|
|
prevThinkingContent: aiMsg.thinkingContent,
|
|
|
};
|
|
|
- pendingApprovalSessionId.value = data.session_id || getCurrentSessionId();
|
|
|
+ pendingApprovalSessionId.value = interruptSessionId;
|
|
|
pendingApprovalThreadId.value = data.thread_id || '';
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
- if (currentTask) {
|
|
|
- currentTask.messages = [...messages.value];
|
|
|
+ // 保存到流式任务的 messages,并同步 UI
|
|
|
+ if (isViewingTask) {
|
|
|
+ streamingTask.messages = [...messages.value];
|
|
|
+ } else {
|
|
|
+ streamingTask.messages = [...streamingTask.messages];
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const parseSSEData = (chunk: string, aiMsgIndex: number) => {
|
|
|
+ const parseSSEData = (chunk: string, aiMsgIndex: number, taskId: string) => {
|
|
|
const lines = chunk.split('\n');
|
|
|
|
|
|
for (const lineRaw of lines) {
|
|
|
@@ -1022,7 +1071,7 @@
|
|
|
try {
|
|
|
const jsonStr = line.substring(5).trim();
|
|
|
const data = JSON.parse(jsonStr);
|
|
|
- handleSSEMessage(data, aiMsgIndex);
|
|
|
+ handleSSEMessage(data, aiMsgIndex, taskId);
|
|
|
} catch (error) {
|
|
|
console.error('解析 SSE data 行失败:', error, line);
|
|
|
}
|
|
|
@@ -1031,17 +1080,19 @@
|
|
|
|
|
|
try {
|
|
|
const data = JSON.parse(line);
|
|
|
- handleSSEMessage(data, aiMsgIndex);
|
|
|
+ handleSSEMessage(data, aiMsgIndex, taskId);
|
|
|
continue;
|
|
|
} catch {
|
|
|
// not json
|
|
|
}
|
|
|
|
|
|
- handleSSEMessage({ type: 'model_thinking', content: line }, aiMsgIndex);
|
|
|
+ handleSSEMessage({ type: 'model_thinking', content: line }, aiMsgIndex, taskId);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const sendTextOnly = async (userInput: string) => {
|
|
|
+ const originTaskId = currentTaskId.value;
|
|
|
+
|
|
|
const now = dayjs();
|
|
|
const userMsg: Message = {
|
|
|
type: 'user',
|
|
|
@@ -1065,37 +1116,46 @@
|
|
|
messages.value.push(aiMsg);
|
|
|
await scrollToBottom();
|
|
|
|
|
|
- const result = await unifiedStream(
|
|
|
- {
|
|
|
- message: userInput,
|
|
|
- session_id: getCurrentSessionId() || undefined,
|
|
|
- mode: editMode.value,
|
|
|
- },
|
|
|
- (chunk: string) => {
|
|
|
- parseSSEData(chunk, aiMsgIndex);
|
|
|
- scrollToBottom();
|
|
|
- }
|
|
|
- );
|
|
|
-
|
|
|
- if (result.session_id) {
|
|
|
- const taskBeforeUpdate = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
- const isNewTask = taskBeforeUpdate && !taskBeforeUpdate.sessionId;
|
|
|
+ const originTask = taskList.value.find((t) => t.id === originTaskId);
|
|
|
+ if (originTask) originTask.isStreaming = true;
|
|
|
|
|
|
- setCurrentSessionId(result.session_id);
|
|
|
+ try {
|
|
|
+ const result = await unifiedStream(
|
|
|
+ {
|
|
|
+ message: userInput,
|
|
|
+ session_id: getCurrentSessionId() || undefined,
|
|
|
+ mode: editMode.value,
|
|
|
+ },
|
|
|
+ (chunk: string) => {
|
|
|
+ parseSSEData(chunk, aiMsgIndex, originTaskId);
|
|
|
+ if (currentTaskId.value === originTaskId) scrollToBottom();
|
|
|
+ }
|
|
|
+ );
|
|
|
|
|
|
- if (isNewTask) {
|
|
|
- taskBeforeUpdate.name = userInput.substring(0, 20) + (userInput.length > 20 ? '...' : '');
|
|
|
+ if (result.session_id) {
|
|
|
+ const originTask = taskList.value.find((t) => t.id === originTaskId);
|
|
|
+ if (originTask && !originTask.sessionId) {
|
|
|
+ originTask.sessionId = result.session_id;
|
|
|
+ originTask.name = userInput.substring(0, 20) + (userInput.length > 20 ? '...' : '');
|
|
|
+ if (originTask.id !== result.session_id) {
|
|
|
+ taskList.value = taskList.value.filter((t) => t.id !== result.session_id);
|
|
|
+ originTask.id = result.session_id;
|
|
|
+ historyLoadedTasks.value.add(result.session_id);
|
|
|
+ }
|
|
|
+ if (currentTaskId.value === originTaskId) {
|
|
|
+ currentTaskId.value = result.session_id;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
- if (currentTask) {
|
|
|
- currentTask.messages = [...messages.value];
|
|
|
+ } finally {
|
|
|
+ const originTask = taskList.value.find((t) => t.id === originTaskId);
|
|
|
+ if (originTask) originTask.isStreaming = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const sendWithAttachment = async (file: AttachedFile, userInput: string) => {
|
|
|
- let currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
+ const originTaskId = currentTaskId.value;
|
|
|
+ let currentTask = taskList.value.find((t) => t.id === originTaskId);
|
|
|
if (currentTask) {
|
|
|
currentTask.attachedFiles.push(file);
|
|
|
} else {
|
|
|
@@ -1130,26 +1190,42 @@
|
|
|
throw new Error('缺少原始文件对象');
|
|
|
}
|
|
|
|
|
|
- const result = await reviewPdfStream(
|
|
|
- {
|
|
|
- file: file.originalFile!,
|
|
|
- session_id: getCurrentSessionId() || undefined,
|
|
|
- message: userInput || undefined,
|
|
|
- mode: editMode.value,
|
|
|
- },
|
|
|
- (chunk: string) => {
|
|
|
- parseSSEData(chunk, aiMsgIndex);
|
|
|
- scrollToBottom();
|
|
|
- }
|
|
|
- );
|
|
|
+ const originTask = taskList.value.find((t) => t.id === originTaskId);
|
|
|
+ if (originTask) originTask.isStreaming = true;
|
|
|
|
|
|
- if (result.session_id) {
|
|
|
- setCurrentSessionId(result.session_id);
|
|
|
- }
|
|
|
+ try {
|
|
|
+ const result = await reviewPdfStream(
|
|
|
+ {
|
|
|
+ file: file.originalFile!,
|
|
|
+ session_id: getCurrentSessionId() || undefined,
|
|
|
+ message: userInput || undefined,
|
|
|
+ mode: editMode.value,
|
|
|
+ },
|
|
|
+ (chunk: string) => {
|
|
|
+ parseSSEData(chunk, aiMsgIndex, originTaskId);
|
|
|
+ if (currentTaskId.value === originTaskId) scrollToBottom();
|
|
|
+ }
|
|
|
+ );
|
|
|
|
|
|
- currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
|
|
|
- if (currentTask) {
|
|
|
- currentTask.messages = [...messages.value];
|
|
|
+ if (result.session_id) {
|
|
|
+ const originTask = taskList.value.find((t) => t.id === originTaskId);
|
|
|
+ if (originTask && !originTask.sessionId) {
|
|
|
+ const taskName = userInput || `文件:${file.name}`;
|
|
|
+ originTask.sessionId = result.session_id;
|
|
|
+ originTask.name = taskName.substring(0, 20) + (taskName.length > 20 ? '...' : '');
|
|
|
+ if (originTask.id !== result.session_id) {
|
|
|
+ taskList.value = taskList.value.filter((t) => t.id !== result.session_id);
|
|
|
+ originTask.id = result.session_id;
|
|
|
+ historyLoadedTasks.value.add(result.session_id);
|
|
|
+ }
|
|
|
+ if (currentTaskId.value === originTaskId) {
|
|
|
+ currentTaskId.value = result.session_id;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ const originTask = taskList.value.find((t) => t.id === originTaskId);
|
|
|
+ if (originTask) originTask.isStreaming = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -1395,6 +1471,61 @@
|
|
|
overflow: hidden;
|
|
|
min-width: 0;
|
|
|
|
|
|
+ .download-confirm-bar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 10px 16px;
|
|
|
+ margin: 0 16px;
|
|
|
+ background: #0a2a3f;
|
|
|
+ border: 1px solid #1a4a6f;
|
|
|
+ border-radius: 8px;
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ .download-confirm-text {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #e0e6ed;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ margin-right: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .download-confirm-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .download-confirm-btn {
|
|
|
+ padding: 4px 14px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ cursor: pointer;
|
|
|
+ border: none;
|
|
|
+ transition: all 0.2s;
|
|
|
+
|
|
|
+ &.download-btn {
|
|
|
+ background: #0a84ff;
|
|
|
+ color: #fff;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: #3a9bfd;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &.cancel-btn {
|
|
|
+ background: rgba(255, 255, 255, 0.08);
|
|
|
+ color: #e0e6ed;
|
|
|
+ border: 1px solid rgba(63, 80, 106, 0.5);
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: rgba(255, 255, 255, 0.15);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
:deep(.chat-header) {
|
|
|
display: flex;
|
|
|
padding: 12px 20px;
|