对话记录和工作空间链路确认

This commit is contained in:
2026-07-07 16:00:08 +08:00
parent 4679280e45
commit 5f0d860d27
6 changed files with 144 additions and 140 deletions
+5 -10
View File
@@ -86,22 +86,16 @@ export interface CreationTreeItem {
// 新的执行列表数据结构
export interface ExecutionItem {
id: string;
timestamp: string;
content: string;
label: string;
type?: string;
}
export interface ExecutionFlowItem {
flowName: string;
Id?: number | string;
sessionId?: string;
items: ExecutionItem[];
}
export interface ExecutionTreeItem {
createDate: string;
flows: ExecutionFlowItem[];
items: ExecutionItem[];
}
export interface CreationListData {
@@ -351,7 +345,7 @@ export function executeFlow(data: ExecuteFlowParams | FormData, requestOptions?:
});
}
/** 删除工作空间结果(单个文件) */
export function deleteExecutionResult(data: { id: string | number }, requestOptions?: RequestOptions) {
export function deleteExecutionResult(data: { id: string; content: string }, requestOptions?: RequestOptions) {
return request({
url: '/ai-agent/flow/execution/deleteResult',
method: 'delete',
@@ -384,10 +378,11 @@ export interface SessionListItem {
}
/** 获取会话记录列表 */
export function getSessionList(requestOptions?: RequestOptions) {
export function getSessionList(params?: Record<string, any>, requestOptions?: RequestOptions) {
return request({
url: '/ai-agent/flow/execution/sessionList',
method: 'get',
params,
requestOptions,
}) as Promise<SessionListResponse>;
}
+3 -3
View File
@@ -39,7 +39,7 @@
<div class="toolbar-right">
<span class="hint-text">Shift+Enter 换行</span>
<button class="send-btn" :disabled="!message.trim() && selectedWorkflowId === null" @click="handleSend">
<button class="send-btn" :disabled="selectedWorkflowId === null" @click="handleSend">
<el-icon><Top /></el-icon>
</button>
</div>
@@ -100,8 +100,8 @@ const fetchWorkflows = async () => {
};
const handleSend = () => {
if (selectedWorkflowId.value === null) return;
const msg = message.value.trim();
if (!msg && selectedWorkflowId.value === null) return;
emit('send', msg || '');
message.value = '';
selectedWorkflowId.value = null;
@@ -133,7 +133,7 @@ onMounted(() => {
fetchWorkflows();
});
defineExpose({ resetAll, clearWorkflow, selectedWorkflowId });
defineExpose({ resetAll, clearWorkflow, selectedWorkflowId, commonWorkflows });
</script>
<style scoped lang="scss">
+9 -5
View File
@@ -395,7 +395,10 @@ watch(
Object.keys(formValues).forEach((key) => delete formValues[key]);
Object.keys(fieldFiles).forEach((key) => delete fieldFiles[key]);
Object.keys(uploadingFields).forEach((key) => delete uploadingFields[key]);
templates.value = [];
// 尝试从执行详情恢复贴片模板
const ext = (detail as any)?.extension;
const restoredTemplates = ext?.templates || (detail as any)?.templates || detail?.flowContent?.templates || [];
templates.value = Array.isArray(restoredTemplates) ? restoredTemplates : [];
if (!detail?.nodeInputParams) return;
detail.nodeInputParams.forEach((node: any) => {
@@ -419,14 +422,15 @@ watch(
}
const key = getFieldKey(node, field);
const hasValue = field.value !== undefined && field.value !== null;
if (field.type === 'number' || field.type === 'inputNumber') {
formValues[key] = field.default ?? null;
formValues[key] = hasValue ? Number(field.value) : (field.default ?? null);
} else if (field.type === 'switch') {
formValues[key] = field.default ?? false;
formValues[key] = hasValue ? Boolean(field.value) : (field.default ?? false);
} else if (field.type === 'upload' || field.type === 'uploadMultiple' || field.type === 'fileUpload') {
formValues[key] = field.default ?? (field.type === 'upload' ? '' : []);
formValues[key] = hasValue ? field.value : (field.default ?? (field.type === 'upload' ? '' : []));
} else {
formValues[key] = field.default ?? '';
formValues[key] = hasValue ? field.value : (field.default ?? '');
}
});
});
+10 -43
View File
@@ -58,21 +58,15 @@
<div v-else class="panel-list">
<div v-for="dateNode in treeNodes" :key="dateNode.id" class="tree-date-group">
<div class="tree-date-label">{{ dateNode.label }}</div>
<div v-for="flowNode in dateNode.children" :key="flowNode.id" class="tree-flow-group">
<div class="tree-flow-label">
<el-icon><Promotion /></el-icon>
<span>{{ flowNode.label }}</span>
</div>
<div v-for="fileNode in flowNode.children" :key="fileNode.id" class="tree-file-item">
<el-icon class="file-type-icon">
<component :is="getFileIcon(fileNode.fileType)" />
</el-icon>
<span class="file-name">{{ fileNode.label }}</span>
<div v-if="fileNode.fileUrl" class="file-actions">
<button class="file-action-btn" @click.stop="handlePreviewNode(fileNode)">预览</button>
<button class="file-action-btn" @click.stop="handleDownloadNode(fileNode)">下载</button>
<button class="file-action-btn file-action-btn--del" @click.stop="handleDeleteNode(fileNode)">删除</button>
</div>
<div v-for="fileNode in dateNode.children" :key="fileNode.id" class="tree-file-item">
<el-icon class="file-type-icon">
<component :is="getFileIcon(fileNode.fileType)" />
</el-icon>
<span class="file-name">{{ fileNode.label }}</span>
<div v-if="fileNode.fileUrl" class="file-actions">
<button class="file-action-btn" @click.stop="handlePreviewNode(fileNode)">预览</button>
<button class="file-action-btn" @click.stop="handleDownloadNode(fileNode)">下载</button>
<button class="file-action-btn file-action-btn--del" @click.stop="handleDeleteNode(fileNode)">删除</button>
</div>
</div>
</div>
@@ -87,7 +81,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Delete, ChatDotRound, ChatLineRound, EditPen, ArrowDown, Promotion, Document, VideoPlay, Headset, Picture, FolderOpened } from '@element-plus/icons-vue';
import { Delete, ChatDotRound, ChatLineRound, EditPen, ArrowDown, Document, VideoPlay, Headset, Picture, FolderOpened } from '@element-plus/icons-vue';
interface TreeNode {
id: string;
@@ -414,33 +408,6 @@ const handleDeleteNode = (data: TreeNode) => emit('delete-node', data);
text-transform: uppercase;
}
.tree-flow-group {
margin-bottom: 2px;
}
.tree-flow-label {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
color: #475569;
.el-icon {
font-size: 13px;
color: #7c3aed;
flex-shrink: 0;
}
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.tree-file-item {
display: flex;
align-items: center;
+111 -64
View File
@@ -17,7 +17,13 @@
@delete-node="handleDeleteNode"
/>
<div class="main-wrapper">
<MainContent ref="mainContentRef" :active-menu="activeMenu" :workflow-detail="selectedWorkflowDetail" :active-history-id="activeHistoryId" @close-workflow="handleCloseWorkflow" />
<MainContent
ref="mainContentRef"
:active-menu="activeMenu"
:workflow-detail="selectedWorkflowDetail"
:active-history-id="activeHistoryId"
@close-workflow="handleCloseWorkflow"
/>
<InputBar ref="inputBarRef" @send="handleSend" @workflow-select="handleWorkflowSelect" />
</div>
@@ -46,8 +52,16 @@ import Sidebar from './components/Sidebar.vue';
import MainContent from './components/MainContent.vue';
import InputBar from './components/InputBar.vue';
import type { ExecutionTreeItem } from '/@/api/settings/creation';
import { getExecutionList, getWorkflowDetail, getExecutionDetail, deleteExecutionResult, deleteExecutionSession, getSessionList, downloadToFile, executeFlow } from '/@/api/settings/creation';
import {
getExecutionList,
getWorkflowDetail,
getExecutionDetail,
deleteExecutionResult,
deleteExecutionSession,
getSessionList,
downloadToFile,
executeFlow,
} from '/@/api/settings/creation';
interface HistoryItem {
id: string;
@@ -70,9 +84,8 @@ interface TreeNode {
nodeType: string;
children?: TreeNode[];
fileUrl?: string;
workflowId?: number | string;
fileType?: string;
sessionId?: string;
backendId?: string;
}
const activeMenu = ref('chat');
@@ -101,27 +114,18 @@ const buildAssetUrl = (p?: string) =>
? joinUrl(joinUrl(apiBaseUrl, imgAddressPrefix.value), p)
: joinUrl(apiBaseUrl, p);
const buildTreeNodes = (tree: ExecutionTreeItem[]): TreeNode[] =>
tree.map((d, di) => ({
id: 'date-' + di,
label: d.createDate,
nodeType: 'date',
children: (d.flows || []).map((f, fi) => ({
id: 'flow-' + di + '-' + fi,
label: f.flowName || '未命名工作流',
nodeType: 'contentType',
workflowId: f.Id,
sessionId: f.sessionId,
children: (f.items || []).map((item, ii) => ({
id: 'item-' + di + '-' + fi + '-' + ii,
label: item.label || '作品' + (ii + 1),
nodeType: 'title',
fileUrl: item.content,
fileType: item.type,
workflowId: f.Id,
sessionId: f.sessionId,
})),
children: (d.items || []).map((item, ii) => ({
id: 'item-' + di + '-' + ii,
label: item.label || '作品' + (ii + 1),
nodeType: 'title',
fileUrl: item.content,
fileType: item.type,
backendId: item.id,
})),
}));
@@ -180,9 +184,9 @@ const handleDeleteNode = async (data: TreeNode) => {
return;
}
// 调后端接口删除
if (data.fileUrl && data.workflowId) {
if (data.backendId && data.fileUrl) {
try {
await deleteExecutionResult({ id: data.workflowId });
await deleteExecutionResult({ id: data.backendId, content: data.fileUrl });
} catch {
// 后端已提示错误,继续本地删除
}
@@ -216,13 +220,11 @@ const getList = async () => {
hasMoreWorkspace.value = true;
treeLoading.value = true;
try {
const [execRes, sessionRes] = await Promise.all([
getExecutionList(),
getSessionList(),
]);
const [execRes, sessionRes] = await Promise.all([getExecutionList({ pageSize: 5 }), getSessionList({ pageSize: 10 })]);
imgAddressPrefix.value = execRes.data?.imgAddressPrefix || '';
treeNodes.value = buildTreeNodes(execRes.data?.tree || []);
const d = new Date(); const todayStr = d.getFullYear() + '-' + String(d.getMonth()+1).padStart(2,'0') + '-' + String(d.getDate()).padStart(2,'0');
const d = new Date();
const todayStr = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
const sessions = getSessionData(sessionRes).map((s) => {
const rawTime = s.createdAt || s.createDate || '';
let displayTime = rawTime;
@@ -262,6 +264,7 @@ const mainContentRef = ref<any>(null);
const workspacePage = ref(1);
const hasMoreWorkspace = ref(true);
const workspaceLoading = ref(false);
const isSending = ref(false);
const inputBarRef = ref<any>(null);
const getSessionId = () => {
@@ -312,8 +315,7 @@ const handleMenuChange = (menu: string) => {
activeMenu.value = menu;
};
const formatTime = (d: Date) =>
String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
const formatTime = (d: Date) => String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
const addMessage = (msg: ChatMessage) => {
const id = activeHistoryId.value;
@@ -324,18 +326,30 @@ const addMessage = (msg: ChatMessage) => {
};
const handleSend = async (message: string) => {
if (isSending.value) return;
isSending.value = true;
if (!selectedWorkflowDetail.value) {
ElMessage.warning('请先选择一个工作流');
isSending.value = false;
return;
}
const mc = mainContentRef.value;
if (!mc) return;
if (!mc) {
isSending.value = false;
return;
}
if (!mc.validateFormFields()) return;
if (!mc.validateFormFields()) {
isSending.value = false;
return;
}
// 获取当前会话的 sessionId
const curSession = historyList.value.find((h) => h.id === activeHistoryId.value);
if (!curSession) return;
if (!curSession) {
isSending.value = false;
return;
}
// 添加用户消息
addMessage({
@@ -351,36 +365,37 @@ const handleSend = async (message: string) => {
try {
// 1. 构建节点输入参数
const nodeInputParams = selectedWorkflowDetail.value.nodeInputParams?.map((node: any) => {
const nodeParam: any = { ...node };
const nodeInputParams =
selectedWorkflowDetail.value.nodeInputParams?.map((node: any) => {
const nodeParam: any = { ...node };
if (node.formConfig && Array.isArray(node.formConfig)) {
nodeParam.formConfig = node.formConfig.map((field: any) => {
// HTTP body 处理
if (String(node.nodeCode || '').toLowerCase() === 'http' && field.field === 'body' && field.value && typeof field.value === 'object') {
const bodyValue = { ...field.value };
Object.entries(bodyValue).forEach(([bodyKey, bodyItem]: [string, any]) => {
if (!bodyItem || bodyItem.showInForm !== true) return;
const bodyFieldKey = `${node.id}_body_${bodyKey}`;
const userVal = mc.formValues[bodyFieldKey];
bodyValue[bodyKey] = {
...bodyItem,
value: userVal !== undefined ? userVal : bodyItem.value,
};
});
return { ...field, value: bodyValue };
}
if (node.formConfig && Array.isArray(node.formConfig)) {
nodeParam.formConfig = node.formConfig.map((field: any) => {
// HTTP body 处理
if (String(node.nodeCode || '').toLowerCase() === 'http' && field.field === 'body' && field.value && typeof field.value === 'object') {
const bodyValue = { ...field.value };
Object.entries(bodyValue).forEach(([bodyKey, bodyItem]: [string, any]) => {
if (!bodyItem || bodyItem.showInForm !== true) return;
const bodyFieldKey = `${node.id}_body_${bodyKey}`;
const userVal = mc.formValues[bodyFieldKey];
bodyValue[bodyKey] = {
...bodyItem,
value: userVal !== undefined ? userVal : bodyItem.value,
};
});
return { ...field, value: bodyValue };
}
const fieldKey = `${node.id}_${field.field || field.label}`;
return {
...field,
value: mc.formValues[fieldKey] !== undefined ? mc.formValues[fieldKey] : field.value,
};
});
}
const fieldKey = `${node.id}_${field.field || field.label}`;
return {
...field,
value: mc.formValues[fieldKey] !== undefined ? mc.formValues[fieldKey] : field.value,
};
});
}
return nodeParam;
}) || [];
return nodeParam;
}) || [];
// 2. 构建 flowContent
const updatedFlowContent = {
@@ -422,10 +437,34 @@ const handleSend = async (message: string) => {
});
ElMessage.success('执行完成');
// 刷新工作空间树
getExecutionList().then((res) => {
getExecutionList({ pageSize: 5 }).then((res) => {
imgAddressPrefix.value = res.data?.imgAddressPrefix || '';
treeNodes.value = buildTreeNodes(res.data?.tree || []);
});
// 虚拟会话执行成功后,刷新会话列表替换真实条目
if (curSession.id.startsWith('virtual_')) {
getSessionList().then((sessionRes) => {
const freshList = getSessionData(sessionRes).map((s: any) => ({
id: s.id,
sessionId: s.sessionId,
title: s.flowName || '未命名会话',
time: s.createDate?.substring(0, 10) || '',
}));
const match = freshList.find((s: any) => s.sessionId === curSession.sessionId);
if (match) {
const idx = historyList.value.findIndex((h) => h.id === curSession.id);
if (idx >= 0) {
const msgs = sessionMessages.value.get(curSession.id);
sessionMessages.value.delete(curSession.id);
sessionMessages.value.set(match.id, msgs || []);
historyList.value[idx] = match;
if (activeHistoryId.value === curSession.id) {
activeHistoryId.value = match.id;
}
}
}
});
}
} catch {
const failSession = historyList.value.find((h) => h.id === activeHistoryId.value);
if (failSession) failSession.status = 'failed';
@@ -435,6 +474,8 @@ const handleSend = async (message: string) => {
time: formatTime(new Date()),
isUser: false,
});
} finally {
isSending.value = false;
}
};
@@ -448,6 +489,12 @@ const handleSelectHistory = async (id: string) => {
const res = await getExecutionDetail(session.id);
if (res.data) {
selectedWorkflowDetail.value = res.data;
// 同步回显 InputBar 的工作流选择
const ib = inputBarRef.value as any;
if (ib?.commonWorkflows && res.data.flowName) {
const match = ib.commonWorkflows.find((w: any) => w.name === res.data.flowName);
if (match) ib.selectedWorkflowId = match.id;
}
}
} catch {
// 无关联执行数据,仅展示消息
@@ -473,7 +520,7 @@ const createNewSession = () => {
activeMenu.value = 'chat';
selectedWorkflowDetail.value = null;
inputBarRef.value?.resetAll();
sessionMessages.value.set(sessionId, []); // 清空消息
sessionMessages.value.set(sessionId, []); // 清空消息
};
const handleCreateHistory = () => {
@@ -532,7 +579,7 @@ onMounted(() => {
radial-gradient(1200px 600px at 0% 0%, rgba(59, 130, 246, 0.18) 0%, rgba(59, 130, 246, 0) 45%),
radial-gradient(1000px 500px at 100% 0%, rgba(139, 92, 246, 0.15) 0%, rgba(139, 92, 246, 0) 50%),
radial-gradient(800px 400px at 0% 100%, rgba(236, 72, 153, 0.12) 0%, rgba(236, 72, 153, 0) 55%),
radial-gradient(600px 300px at 100% 100%, rgba(16, 185, 129, 0.10) 0%, rgba(16, 185, 129, 0) 60%),
radial-gradient(600px 300px at 100% 100%, rgba(16, 185, 129, 0.1) 0%, rgba(16, 185, 129, 0) 60%),
linear-gradient(135deg, #f8fbff 0%, #e6f0ff 60%, #f0f4fa 100%);
}
@@ -564,4 +611,4 @@ onMounted(() => {
width: 100%;
min-height: 60vh;
}
</style>
</style>
+6 -15
View File
@@ -1401,21 +1401,12 @@ const buildTreeNodes = (tree: ExecutionTreeItem[]): TreeNode[] =>
id: `date-${di}`,
label: d.createDate,
nodeType: 'date',
children: (d.flows || []).map((f, fi) => ({
id: `flow-${di}-${fi}`,
label: f.flowName || '未命名工作流',
nodeType: 'contentType',
workflowId: f.Id,
sessionId: f.sessionId,
children: (f.items || []).map((item, ii) => ({
id: `item-${di}-${fi}-${ii}`,
label: item.label || `作品${ii + 1}`,
nodeType: 'title',
fileUrl: item.content,
fileType: item.type,
workflowId: f.Id,
sessionId: f.sessionId,
})),
children: (d.items || []).map((item, ii) => ({
id: `item-${di}-${ii}`,
label: item.label || `作品${ii + 1}`,
nodeType: 'title',
fileUrl: item.content,
fileType: item.type,
})),
}));
const getList = async () => {