会话相关
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
import request from '/@/utils/request';
|
||||
|
||||
/**
|
||||
* 查询会话问答消息记录(普通对话历史)。
|
||||
* 依赖后端接口,URL/返回结构待后端提供;接口未就绪前由调用方以 localStorage 兜底。
|
||||
*/
|
||||
export function getSessionMessages(sessionId: string) {
|
||||
return request({
|
||||
url: '/ai-agent/session/getMessages',
|
||||
method: 'get',
|
||||
params: { sessionId },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import request, { type RequestOptions } from '/@/utils/request';
|
||||
|
||||
// ===== 会话管理(/ai-agent/session/*)=====
|
||||
// 后端结构:
|
||||
// ListSessionReq { page } → ListSessionRes { list: VOSession[], total }
|
||||
// GetSessionInfoReq { page, sessionId } → GetSessionInfoRes { list: VOSessionInfoResult[], total }
|
||||
// DeleteSessionReq { id }(POST,级联删除普通对话结果,工作流结果保留)
|
||||
// 说明:会话为「会话 → 会话内结果(工作流/普通对话混排,按时间倒序)」模型。
|
||||
|
||||
export interface VOSession {
|
||||
id: string;
|
||||
sessionName: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface VOSessionInfoResult {
|
||||
id: string;
|
||||
type: 'workflow' | 'chat';
|
||||
status: number; // 1-运行中, 2-成功, 3-失败
|
||||
flowId: string;
|
||||
requestParams: Record<string, any>;
|
||||
resultFileUrl: string;
|
||||
totalTokens: number;
|
||||
totalFee: number;
|
||||
errorMsg: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export function getSessionListV2(params?: Record<string, any>, requestOptions?: RequestOptions) {
|
||||
return request({
|
||||
url: '/ai-agent/session/list',
|
||||
method: 'get',
|
||||
params,
|
||||
requestOptions,
|
||||
}) as Promise<{ code: number; message: string; data: { list: VOSession[]; total: number } }>;
|
||||
}
|
||||
|
||||
export function getSessionResults(sessionId: string, params?: Record<string, any>, requestOptions?: RequestOptions) {
|
||||
return request({
|
||||
url: '/ai-agent/session/get',
|
||||
method: 'get',
|
||||
params: { sessionId, ...params },
|
||||
requestOptions,
|
||||
}) as Promise<{ code: number; message: string; data: { list: VOSessionInfoResult[]; total: number } }>;
|
||||
}
|
||||
|
||||
export function deleteSessionV2(id: string, requestOptions?: RequestOptions) {
|
||||
return request({
|
||||
url: '/ai-agent/session/delete',
|
||||
method: 'post',
|
||||
data: { id },
|
||||
requestOptions,
|
||||
});
|
||||
}
|
||||
@@ -40,6 +40,9 @@
|
||||
</div>
|
||||
<!-- AI 消息操作栏(hover 显示) -->
|
||||
<div v-if="!msg.isUser && msg.content && !isErrorMsg(msg)" class="msg-actions">
|
||||
<button class="msg-action" title="重新生成" @click="emit('regenerate', msg)">
|
||||
<el-icon><RefreshRight /></el-icon>
|
||||
</button>
|
||||
<button class="msg-action" title="复制回答" @click="copyText(msg.content)">
|
||||
<el-icon><DocumentCopy /></el-icon>
|
||||
</button>
|
||||
@@ -53,7 +56,7 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, reactive, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { DocumentCopy } from '@element-plus/icons-vue';
|
||||
import { DocumentCopy, RefreshRight } from '@element-plus/icons-vue';
|
||||
import 'highlight.js/styles/github-dark.css';
|
||||
import { renderMarkdown } from '../utils/markdown';
|
||||
|
||||
@@ -70,6 +73,7 @@ interface ChatMessage {
|
||||
|
||||
interface Emits {
|
||||
(e: 'retry', msg: ChatMessage): void;
|
||||
(e: 'regenerate', msg: ChatMessage): void;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -41,8 +41,14 @@
|
||||
|
||||
<div class="toolbar-right">
|
||||
<span class="hint-text">Shift+Enter 换行</span>
|
||||
<button class="send-btn" :disabled="sendDisabled" @click="handleSend">
|
||||
<el-icon><Top /></el-icon>
|
||||
<button
|
||||
class="send-btn"
|
||||
:class="{ 'is-stop': generating }"
|
||||
:disabled="!generating && sendDisabled"
|
||||
@click="generating ? emit('stop') : handleSend()"
|
||||
>
|
||||
<el-icon v-if="generating"><VideoPause /></el-icon>
|
||||
<el-icon v-else><Top /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,7 +75,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { Top, MagicStick, Promotion, Close, Paperclip } from '@element-plus/icons-vue';
|
||||
import { Top, MagicStick, Promotion, Close, Paperclip, VideoPause } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getWorkflowList } from '/@/api/settings/creation';
|
||||
|
||||
@@ -77,11 +83,13 @@ interface Props {
|
||||
sendDisabled?: boolean;
|
||||
workflowLocked?: boolean;
|
||||
hideShortcuts?: boolean;
|
||||
generating?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'send', message: string): void;
|
||||
(e: 'workflow-select', workflowId: string | null, isTemplate?: boolean): void;
|
||||
(e: 'stop'): void;
|
||||
}
|
||||
|
||||
interface Workflow {
|
||||
@@ -96,6 +104,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
sendDisabled: false,
|
||||
workflowLocked: false,
|
||||
hideShortcuts: false,
|
||||
generating: false,
|
||||
});
|
||||
const emit = defineEmits<Emits>();
|
||||
const message = ref('');
|
||||
@@ -384,6 +393,16 @@ const onCapsuleAfterLeave = (el: Element) => {
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* 生成中:变停止按钮 */
|
||||
&.is-stop {
|
||||
background: #f43f5e;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: #e11d48;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 快捷工作流胶囊 */
|
||||
|
||||
@@ -130,9 +130,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 对话页 — 无工作流但有消息时展示消息流 -->
|
||||
<div v-else-if="hasMessages" :key="'chat'" class="content-body chat-body">
|
||||
<ChatList :messages="messages" @retry="emit('retry', $event)" />
|
||||
<!-- 对话页 — 无工作流但有消息或会话结果时展示:结果卡片(仅工作流结果时显示)+ 消息流(下) -->
|
||||
<div v-else-if="hasMessages || hasResults" :key="'chat'" class="content-body chat-body">
|
||||
<SessionResults
|
||||
v-if="hasWorkflowResults"
|
||||
:results="results"
|
||||
:loading="resultsLoading"
|
||||
@preview="emit('preview-result', $event)"
|
||||
@download="emit('download-result', $event)"
|
||||
/>
|
||||
<ChatList
|
||||
:messages="messages"
|
||||
class="chat-list-scroll"
|
||||
@retry="emit('retry', $event)"
|
||||
@regenerate="emit('regenerate', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 默认占位 — 无工作流时显示引导 -->
|
||||
@@ -170,26 +182,37 @@ import { collectHomeFormFields } from '../utils/flowDsl';
|
||||
import { getWorkflowList } from '/@/api/settings/creation';
|
||||
import { Promotion } from '@element-plus/icons-vue';
|
||||
import ChatList from './ChatList.vue';
|
||||
import SessionResults from './SessionResults.vue';
|
||||
import type { VOSessionInfoResult } from '/@/api/settings/workflow/session';
|
||||
|
||||
interface Props {
|
||||
activeMenu: string;
|
||||
workflowDetail: any;
|
||||
activeHistoryId?: string | null;
|
||||
messages?: any[];
|
||||
results?: VOSessionInfoResult[];
|
||||
resultsLoading?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'retry', msg: any): void;
|
||||
(e: 'regenerate', msg: any): void;
|
||||
(e: 'workflow-select', id: string, isTemplate?: boolean): void;
|
||||
(e: 'preview-result', url: string): void;
|
||||
(e: 'download-result', url: string): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
activeHistoryId: null,
|
||||
messages: () => [],
|
||||
results: () => [],
|
||||
resultsLoading: false,
|
||||
});
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const hasMessages = computed(() => Array.isArray(props.messages) && props.messages.length > 0);
|
||||
const hasResults = computed(() => Array.isArray(props.results) && props.results.length > 0);
|
||||
const hasWorkflowResults = computed(() => Array.isArray(props.results) && props.results.some((r) => r.type === 'workflow'));
|
||||
const formValues = reactive<Record<string, any>>({});
|
||||
const fieldFiles = reactive<Record<string, { name: string; url: string }[]>>({});
|
||||
const uploadingFields = reactive<Record<string, boolean>>({});
|
||||
@@ -593,11 +616,27 @@ defineExpose({ formValues, fieldFiles, templates, validateFormFields });
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden; /* 结果列表固定高度、消息流内部滚动,避免整体滚动与内部冲突 */
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
}
|
||||
|
||||
/* 会话结果区块:固定在消息流上方,结果多时内部滚动(不挤压消息流) */
|
||||
.chat-body :deep(.session-results) {
|
||||
flex-shrink: 0;
|
||||
max-height: 42%;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
}
|
||||
|
||||
/* 消息流填充剩余高度并在内部滚动 */
|
||||
.chat-body :deep(.chat-list-scroll) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.placeholder-body { display: flex; align-items: center; justify-content: center; }
|
||||
.placeholder-content { text-align: center; padding: 24px 20px; width: 100%; max-width: 740px; }
|
||||
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
<template>
|
||||
<div class="session-results">
|
||||
<div class="results-header">
|
||||
<span class="results-title">会话结果</span>
|
||||
<span v-if="results.length" class="results-count">{{ results.length }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading && results.length === 0" class="results-empty">加载中…</div>
|
||||
<!-- 空态 -->
|
||||
<div v-else-if="results.length === 0" class="results-empty">暂无会话结果</div>
|
||||
<!-- 结果列表 -->
|
||||
<div v-else class="results-list">
|
||||
<div v-for="r in results" :key="r.id" class="result-card" :class="`is-${statusKey(r.status)}`">
|
||||
<div class="result-main">
|
||||
<span class="result-type">{{ r.type === 'workflow' ? '工作流' : '对话' }}</span>
|
||||
<span class="result-status" :class="`is-${statusKey(r.status)}`">
|
||||
<i v-if="r.status === 1" class="status-dot"></i>{{ statusText(r.status) }}
|
||||
</span>
|
||||
<span v-if="r.totalTokens" class="result-meta">{{ r.totalTokens }} tokens</span>
|
||||
<span v-if="r.totalFee" class="result-meta">¥{{ r.totalFee }}</span>
|
||||
<span class="result-time">{{ r.createdAt || '' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 问题 / 参数摘要 -->
|
||||
<div v-if="getQuestionText(r)" class="result-question">{{ getQuestionText(r) }}</div>
|
||||
|
||||
<!-- AI 回复(resultFileUrl 指向的 txt 内容,进入自动读取) -->
|
||||
<div v-if="r.resultFileUrl" class="result-reply">
|
||||
<div v-if="getReplyState(r)?.loading" class="result-reply-tip">加载回复中…</div>
|
||||
<div v-else-if="getReplyState(r)?.error" class="result-reply-tip is-error">回复加载失败</div>
|
||||
<div v-else-if="getReplyState(r)?.text" class="result-reply-text">{{ getReplyState(r)?.text }}</div>
|
||||
<div v-else class="result-reply-tip">暂无回复内容</div>
|
||||
</div>
|
||||
|
||||
<div v-if="r.errorMsg || r.resultFileUrl" class="result-sub">
|
||||
<span v-if="r.errorMsg" class="result-error">❌ {{ r.errorMsg }}</span>
|
||||
<div v-if="r.type === 'workflow' && r.resultFileUrl" class="result-actions">
|
||||
<button type="button" class="result-action" @click="emit('preview', r.resultFileUrl)">预览</button>
|
||||
<button type="button" class="result-action" @click="emit('download', r.resultFileUrl)">下载</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from 'vue';
|
||||
import { getUpFileUrl } from '/@/utils/gfast';
|
||||
import type { VOSessionInfoResult } from '/@/api/settings/workflow/session';
|
||||
|
||||
interface Props {
|
||||
results: VOSessionInfoResult[];
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'preview', url: string): void;
|
||||
(e: 'download', url: string): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
loading: false,
|
||||
});
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 状态键:1-运行中, 2-成功, 3-失败
|
||||
const statusKey = (status: number): string => {
|
||||
if (status === 2) return 'success';
|
||||
if (status === 3) return 'failed';
|
||||
return 'running';
|
||||
};
|
||||
|
||||
const statusText = (status: number): string => {
|
||||
if (status === 2) return '成功';
|
||||
if (status === 3) return '失败';
|
||||
return '运行中';
|
||||
};
|
||||
|
||||
// 卡片问题/参数摘要:chat 取 requestParams.question,其余输出参数 JSON 摘要
|
||||
const getQuestionText = (r: VOSessionInfoResult): string => {
|
||||
const q = r.requestParams?.question;
|
||||
if (q) return String(q);
|
||||
const params = r.requestParams;
|
||||
if (params && Object.keys(params).length > 0) {
|
||||
try {
|
||||
return JSON.stringify(params);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// 结果卡片回复内容:进入自动 fetch resultFileUrl 指向的 txt 文件并展示
|
||||
const replyMap = reactive<Record<string, { loading: boolean; text: string; error: boolean }>>({});
|
||||
const getReplyState = (r: VOSessionInfoResult) => replyMap[r.id];
|
||||
|
||||
const loadReply = async (r: VOSessionInfoResult) => {
|
||||
if (!r.resultFileUrl) return;
|
||||
if (replyMap[r.id] && (replyMap[r.id].loading || replyMap[r.id].text)) return; // 加载中/已有内容不重复请求
|
||||
replyMap[r.id] = { loading: true, text: '', error: false };
|
||||
try {
|
||||
const res = await fetch(getUpFileUrl(r.resultFileUrl));
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
const text = await res.text();
|
||||
replyMap[r.id] = { loading: false, text, error: false };
|
||||
} catch {
|
||||
replyMap[r.id] = { loading: false, text: '', error: true };
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.results,
|
||||
(list) => {
|
||||
(list || []).forEach((r) => loadReply(r));
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.session-results {
|
||||
width: 100%;
|
||||
max-width: 880px;
|
||||
margin: 0 auto;
|
||||
padding: 12px 0 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.results-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.results-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.results-count {
|
||||
font-size: 11px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.results-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.results-empty {
|
||||
font-size: 12px;
|
||||
color: #cbd5e1;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
background: #fff;
|
||||
border: 1px solid #eef0f3;
|
||||
border-radius: 10px;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
|
||||
&.is-running {
|
||||
border-color: #dbeafe;
|
||||
background: #f8fbff;
|
||||
}
|
||||
&.is-failed {
|
||||
border-color: #fee2e2;
|
||||
background: #fef6f6;
|
||||
}
|
||||
}
|
||||
|
||||
.result-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.result-type {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
background: #f1f5f9;
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
|
||||
.result-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&.is-running {
|
||||
color: #2563eb;
|
||||
}
|
||||
&.is-success {
|
||||
color: #16a34a;
|
||||
}
|
||||
&.is-failed {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #2563eb;
|
||||
animation: status-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes status-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.35;
|
||||
}
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.result-time {
|
||||
font-size: 11px;
|
||||
color: #cbd5e1;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.result-sub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.result-error {
|
||||
font-size: 12px;
|
||||
color: #dc2626;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.result-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.result-action {
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #fff;
|
||||
color: #3b82f6;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #eff6ff;
|
||||
border-color: #bfdbfe;
|
||||
}
|
||||
}
|
||||
|
||||
/* 问题/参数摘要 */
|
||||
.result-question {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
padding: 2px 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* AI 回复区(txt 内容) */
|
||||
.result-reply {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.result-reply-text {
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #f1f5f9;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
&::-webkit-scrollbar { width: 4px; }
|
||||
&::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
|
||||
}
|
||||
|
||||
.result-reply-tip {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
padding: 4px 0;
|
||||
&.is-error { color: #dc2626; }
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
<el-icon class="brand-icon"><ChatDotRound /></el-icon>
|
||||
<span class="brand-name">AI 助手</span>
|
||||
</div>
|
||||
<el-tooltip content="新建对话" placement="right">
|
||||
<el-tooltip content="新建会话" placement="right">
|
||||
<button class="new-chat-icon-btn" @click="handleNewChat">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</button>
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="sidebar-tabs">
|
||||
<button class="tab-btn" :class="{ active: activeTab === 'history' }" @click="activeTab = 'history'">
|
||||
<el-icon><ChatLineRound /></el-icon>
|
||||
对话记录
|
||||
会话记录
|
||||
</button>
|
||||
<button class="tab-btn" :class="{ active: activeTab === 'workspace' }" @click="activeTab = 'workspace'">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div v-else class="panel-empty">暂无对话记录</div>
|
||||
<div v-else class="panel-empty">暂无会话记录</div>
|
||||
</div>
|
||||
|
||||
<!-- 工作空间面板 -->
|
||||
|
||||
+217
-111
@@ -21,10 +21,24 @@
|
||||
:workflow-detail="selectedWorkflowDetail"
|
||||
:active-history-id="activeHistoryId"
|
||||
:messages="currentMessages"
|
||||
:results="currentSessionResults"
|
||||
:results-loading="currentSessionResultsLoading"
|
||||
@retry="handleRetry"
|
||||
@workflow-select="handlePlaceWorkflowSelect"
|
||||
@regenerate="handleRetry"
|
||||
@preview-result="handlePreviewResult"
|
||||
@download-result="handleDownloadResult"
|
||||
@workflow-select="handlePlaceWorkflowSelect"
|
||||
/>
|
||||
<InputBar
|
||||
ref="inputBarRef"
|
||||
:send-disabled="isSendDisabled"
|
||||
:workflow-locked="isHistoryWorkflow"
|
||||
:hide-shortcuts="isPlaceholder"
|
||||
:generating="isGenerating"
|
||||
@send="handleSend"
|
||||
@workflow-select="handleWorkflowSelect"
|
||||
@stop="handleStopGenerate"
|
||||
/>
|
||||
<InputBar ref="inputBarRef" :send-disabled="isSendDisabled" :workflow-locked="isHistoryWorkflow" :hide-shortcuts="isPlaceholder" @send="handleSend" @workflow-select="handleWorkflowSelect" />
|
||||
</div>
|
||||
|
||||
<!-- 预览弹窗 -->
|
||||
@@ -63,17 +77,17 @@ import { applyHomeFormValues } from './utils/flowDsl';
|
||||
import { getChatModel } from '/@/api/settings/modelConfigV2';
|
||||
import { openWsExecute } from './utils/wsExecute';
|
||||
import { parseWsMessage, getDelta, getAnswer, getErrorText, getToolCallName, getToolResultText } from './utils/wsMessage';
|
||||
import { getSessionMessages } from '/@/api/settings/session';
|
||||
import type { ExecutionTreeItem } from '/@/api/settings/creation';
|
||||
import {
|
||||
getExecutionList,
|
||||
getWorkflowDetail,
|
||||
getExecutionDetail,
|
||||
deleteExecutionResult,
|
||||
deleteExecutionSession,
|
||||
getSessionList,
|
||||
downloadToFile,
|
||||
} from '/@/api/settings/creation';
|
||||
import { getSessionListV2, getSessionResults, deleteSessionV2 } from '/@/api/settings/workflow/session';
|
||||
import type { VOSessionInfoResult } from '/@/api/settings/workflow/session';
|
||||
import { getUpFileUrl } from '/@/utils/gfast';
|
||||
|
||||
interface HistoryItem {
|
||||
id: string;
|
||||
@@ -161,26 +175,46 @@ const previewNode = (data: TreeNode) => {
|
||||
previewDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const downloadNode = async (data: TreeNode) => {
|
||||
if (!data.fileUrl) return;
|
||||
const url = buildAssetUrl(data.fileUrl);
|
||||
const getUrlExt = (url: string): string => {
|
||||
const m = String(url || '').split('?')[0].toLowerCase().match(/\.([a-z0-9]+)$/);
|
||||
return m ? m[1] : '';
|
||||
};
|
||||
|
||||
// 会话结果文件预览(复用预览弹窗)
|
||||
const handlePreviewResult = (url: string) => {
|
||||
if (!url) return;
|
||||
previewUrl.value = buildAssetUrl(url);
|
||||
previewMode.value = getPreviewMode(getUrlExt(url));
|
||||
previewDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 会话结果文件下载
|
||||
const handleDownloadResult = (url: string) => {
|
||||
if (!url) return;
|
||||
const name = String(url).split('/').pop()?.split('?')[0] || 'result';
|
||||
downloadAsset(url, name);
|
||||
};
|
||||
|
||||
// 通用文件下载:优先后端代理,兜底 a 标签
|
||||
const downloadAsset = async (url: string, name: string) => {
|
||||
try {
|
||||
// 优先通过后端代理下载,避免跨域问题
|
||||
const res = await downloadToFile({ fileURL: data.fileUrl });
|
||||
const res = await downloadToFile({ fileURL: url });
|
||||
const blob = res.data as Blob;
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = blobUrl;
|
||||
a.download = data.label;
|
||||
a.download = name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
} catch {
|
||||
// 兜底:直接用 a 标签下载
|
||||
const full = buildAssetUrl(url);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = data.label;
|
||||
a.href = full;
|
||||
a.download = name;
|
||||
a.target = '_blank';
|
||||
a.rel = 'noopener';
|
||||
document.body.appendChild(a);
|
||||
@@ -189,6 +223,11 @@ const downloadNode = async (data: TreeNode) => {
|
||||
}
|
||||
};
|
||||
|
||||
const downloadNode = async (data: TreeNode) => {
|
||||
if (!data.fileUrl) return;
|
||||
await downloadAsset(data.fileUrl, data.label);
|
||||
};
|
||||
|
||||
const handleDeleteNode = async (data: TreeNode) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除「${data.label}」?此操作不可恢复。`, '确认删除', {
|
||||
@@ -238,30 +277,33 @@ const getSessionData = (res: any): any[] => {
|
||||
const getList = async () => {
|
||||
treeLoading.value = true;
|
||||
try {
|
||||
const [execRes, sessionRes] = await Promise.all([getExecutionList(), getSessionList({ pageSize: 10 })]);
|
||||
const [execRes, sessionRes] = await Promise.all([getExecutionList(), getSessionListV2({ pageNum: 1, 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 sessions = getSessionData(sessionRes).map((s) => {
|
||||
const rawTime = s.createdAt || s.createDate || '';
|
||||
let displayTime = rawTime;
|
||||
if (rawTime) {
|
||||
const datePart = rawTime.substring(0, 10);
|
||||
if (datePart === todayStr) {
|
||||
displayTime = rawTime.substring(11, 16);
|
||||
} else {
|
||||
displayTime = datePart;
|
||||
const sessions = getSessionData(sessionRes)
|
||||
// 后端个别会话 sessionId 可能为空串,空 id 无法点击查询详情,过滤掉
|
||||
.filter((s) => s.sessionId)
|
||||
.map((s) => {
|
||||
const rawTime = s.createdAt || '';
|
||||
let displayTime = rawTime;
|
||||
if (rawTime) {
|
||||
const datePart = rawTime.substring(0, 10);
|
||||
if (datePart === todayStr) {
|
||||
displayTime = rawTime.substring(11, 16);
|
||||
} else {
|
||||
displayTime = datePart;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: s.id,
|
||||
sessionId: s.sessionId,
|
||||
title: s.flowName || '未命名会话',
|
||||
time: displayTime,
|
||||
status: mapSessionStatus(s.status),
|
||||
};
|
||||
});
|
||||
return {
|
||||
id: String(s.sessionId),
|
||||
sessionId: String(s.sessionId),
|
||||
title: s.sessionName || '未命名会话',
|
||||
time: displayTime,
|
||||
status: mapSessionStatus(s.status),
|
||||
};
|
||||
});
|
||||
historyList.value = sessions;
|
||||
// 为每个后端会话初始化消息存储
|
||||
sessions.forEach((s) => {
|
||||
@@ -281,7 +323,13 @@ const getList = async () => {
|
||||
const selectedWorkflowDetail = ref<any>(null);
|
||||
const mainContentRef = ref<any>(null);
|
||||
const sendingSessions = reactive<Record<string, boolean>>({});
|
||||
// 当前正在执行的 WebSocket(用于停止生成);cancelledWs 记录被主动取消的连接,避免其 onClose 误判为正常完成
|
||||
const currentWs = ref<WebSocket | null>(null);
|
||||
const cancelledWs = new Set<WebSocket>();
|
||||
const isHistoryWorkflow = ref(false);
|
||||
|
||||
// 生成中:有活跃会话且该会话正处于发送状态(发送按钮切换为停止按钮)
|
||||
const isGenerating = computed(() => !!activeHistoryId.value && !!sendingSessions[activeHistoryId.value]);
|
||||
const inputBarRef = ref<any>(null);
|
||||
const templateDialogVisible = ref(false);
|
||||
const pendingTemplate = ref<any>(null);
|
||||
@@ -300,6 +348,18 @@ const currentMessages = computed(() => {
|
||||
return sessionMessages.value.get(id) || [];
|
||||
});
|
||||
|
||||
// 会话内结果(session/get,workflow+chat 混排):key = 会话 id
|
||||
const sessionResultsMap = reactive<Record<string, VOSessionInfoResult[]>>({});
|
||||
const sessionResultsLoading = reactive<Record<string, boolean>>({});
|
||||
const currentSessionResults = computed(() => {
|
||||
const id = activeHistoryId.value;
|
||||
return id ? sessionResultsMap[id] || [] : [];
|
||||
});
|
||||
const currentSessionResultsLoading = computed(() => {
|
||||
const id = activeHistoryId.value;
|
||||
return !!id && !!sessionResultsLoading[id];
|
||||
});
|
||||
|
||||
// 占位态:无工作流且无消息(MainContent 显示引导页)时隐藏输入框快捷胶囊,避免与占位卡片重复
|
||||
const isPlaceholder = computed(() => !selectedWorkflowDetail.value && currentMessages.value.length === 0);
|
||||
|
||||
@@ -355,39 +415,12 @@ const handleMenuChange = (menu: string) => {
|
||||
|
||||
const formatTime = (d: Date) => String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
|
||||
|
||||
// ===== 消息缓存(普通对话历史;后端接口就绪前 localStorage 兜底)=====
|
||||
const CHAT_CACHE_KEY = 'home_chat_msgs_';
|
||||
const chatCacheKey = (sid: string) => CHAT_CACHE_KEY + sid;
|
||||
const loadChatCache = (sid: string): ChatMessage[] => {
|
||||
try {
|
||||
const raw = localStorage.getItem(chatCacheKey(sid));
|
||||
return raw ? (JSON.parse(raw) as ChatMessage[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
const saveChatCache = (sid: string, msgs: ChatMessage[]) => {
|
||||
try {
|
||||
localStorage.setItem(chatCacheKey(sid), JSON.stringify(msgs));
|
||||
} catch {
|
||||
/* 存储不可用时忽略 */
|
||||
}
|
||||
};
|
||||
const clearChatCache = (sid: string) => {
|
||||
try {
|
||||
localStorage.removeItem(chatCacheKey(sid));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
const addMessage = (msg: ChatMessage) => {
|
||||
const id = activeHistoryId.value;
|
||||
if (!id) return;
|
||||
const list = sessionMessages.value.get(id);
|
||||
if (list) list.push(msg);
|
||||
else sessionMessages.value.set(id, [msg]);
|
||||
saveChatCache(id, sessionMessages.value.get(id) || []);
|
||||
};
|
||||
|
||||
const handleSend = async (message: string) => {
|
||||
@@ -473,7 +506,6 @@ const handleRetry = async (msg: ChatMessage) => {
|
||||
if (!question) return;
|
||||
// 移除失败消息,重新执行
|
||||
list.splice(idx, 1);
|
||||
saveChatCache(sid, list);
|
||||
sendingSessions[sid] = true;
|
||||
session.status = 'executing';
|
||||
const sessionId = session.sessionId || getSessionId();
|
||||
@@ -486,6 +518,42 @@ const handleRetry = async (msg: ChatMessage) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ===== 停止生成:发送 workflow_cancel 并关闭连接,保留已生成内容 =====
|
||||
const handleStopGenerate = () => {
|
||||
const sid = activeHistoryId.value;
|
||||
if (!sid || !sendingSessions[sid]) return;
|
||||
const ws = currentWs.value;
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
cancelledWs.add(ws);
|
||||
try {
|
||||
ws.send(JSON.stringify({ type: 'workflow_cancel' }));
|
||||
} catch {
|
||||
/* 连接异常时忽略,直接关闭 */
|
||||
}
|
||||
ws.close();
|
||||
}
|
||||
currentWs.value = null;
|
||||
// 标记正在生成的 AI 消息:已有内容则保留;无内容则显示停止提示
|
||||
const list = sessionMessages.value.get(sid);
|
||||
const last = list && list.length > 0 ? list[list.length - 1] : null;
|
||||
if (last && !last.isUser) {
|
||||
last.loading = false;
|
||||
if (!last.content) last.content = '⚠️ 已停止生成';
|
||||
} else if (list) {
|
||||
// 工作流执行模式:无 AI 气泡,追加一条停止提示
|
||||
addMessage({
|
||||
id: 'msg-' + Date.now() + '-stop',
|
||||
content: '⚠️ 已停止生成',
|
||||
time: formatTime(new Date()),
|
||||
isUser: false,
|
||||
});
|
||||
}
|
||||
// 清理会话状态(runChat 的 onClose 会再走一次 finishChat,幂等)
|
||||
const session = historyList.value.find((h) => h.id === sid);
|
||||
if (session && session.status === 'executing') session.status = 'completed';
|
||||
delete sendingSessions[sid];
|
||||
};
|
||||
|
||||
// ===== 工作流执行:选中工作流 → 表单页 WS 执行 → 完成后切回对话页 =====
|
||||
const runWorkflow = async (sid: string, sessionId: string, message: string, mc: any) => {
|
||||
const curSession = historyList.value.find((h) => h.id === sid);
|
||||
@@ -518,13 +586,15 @@ const runWorkflow = async (sid: string, sessionId: string, message: string, mc:
|
||||
}
|
||||
// 虚拟会话执行后刷新会话列表替换真实条目
|
||||
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) || '',
|
||||
}));
|
||||
getSessionListV2({ pageNum: 1, pageSize: 10 }).then((sessionRes) => {
|
||||
const freshList = getSessionData(sessionRes)
|
||||
.filter((s: any) => s.sessionId)
|
||||
.map((s: any) => ({
|
||||
id: String(s.sessionId),
|
||||
sessionId: String(s.sessionId),
|
||||
title: s.sessionName || '未命名会话',
|
||||
time: s.createdAt?.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);
|
||||
@@ -537,8 +607,12 @@ const runWorkflow = async (sid: string, sessionId: string, message: string, mc:
|
||||
activeHistoryId.value = match.id;
|
||||
}
|
||||
}
|
||||
loadSessionResults(match.id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 非虚拟会话:执行完成/失败后直接刷新会话结果
|
||||
loadSessionResults(sid);
|
||||
}
|
||||
delete sendingSessions[sid];
|
||||
// 执行完成后切回对话页:清空工作流选择,主区域展示消息流
|
||||
@@ -597,10 +671,14 @@ const runWorkflow = async (sid: string, sessionId: string, message: string, mc:
|
||||
finishExec(false, '执行连接失败,请重试');
|
||||
},
|
||||
onClose: () => {
|
||||
// 主动停止(handleStopGenerate 已发 workflow_cancel 并 close)时跳过,
|
||||
// 避免被误判为正常完成(工作流完成/失败提示由停止逻辑单独处理)
|
||||
if (ws && cancelledWs.has(ws)) return;
|
||||
// 连接关闭兜底视为完成;若已触发 onError 则按失败处理
|
||||
if (!finished) finishExec(!wsFailed);
|
||||
},
|
||||
});
|
||||
currentWs.value = ws;
|
||||
if (!ws) {
|
||||
finishExec(false, 'WebSocket 初始化失败,请检查服务地址');
|
||||
}
|
||||
@@ -634,7 +712,6 @@ const runChat = async (sid: string, sessionId: string, message: string) => {
|
||||
const ai = aiIdx >= 0 && list ? list[aiIdx] : null;
|
||||
if (ai && ai.thinking === '思考中…') {
|
||||
ai.thinking = '';
|
||||
saveChatCache(sid, list || []);
|
||||
}
|
||||
twBuffer += text;
|
||||
startTypewriter();
|
||||
@@ -658,7 +735,6 @@ const runChat = async (sid: string, sessionId: string, message: string) => {
|
||||
ai.thinking = (ai.thinking || '') + twBuffer.slice(0, take);
|
||||
twBuffer = twBuffer.slice(take);
|
||||
ai.loading = false;
|
||||
saveChatCache(sid, list || []);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
@@ -677,7 +753,6 @@ const runChat = async (sid: string, sessionId: string, message: string) => {
|
||||
const ai = aiIdx >= 0 && list ? list[aiIdx] : null;
|
||||
if (ai) {
|
||||
ai.thinking = (ai.thinking || '') + twBuffer;
|
||||
saveChatCache(sid, list || []);
|
||||
}
|
||||
twBuffer = '';
|
||||
};
|
||||
@@ -710,13 +785,15 @@ const runChat = async (sid: string, sessionId: string, message: string) => {
|
||||
if (session) session.status = success ? 'completed' : 'failed';
|
||||
// 虚拟会话对话后刷新会话列表替换真实条目
|
||||
if (session && session.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) || '',
|
||||
}));
|
||||
getSessionListV2({ pageNum: 1, pageSize: 10 }).then((sessionRes) => {
|
||||
const freshList = getSessionData(sessionRes)
|
||||
.filter((s: any) => s.sessionId)
|
||||
.map((s: any) => ({
|
||||
id: String(s.sessionId),
|
||||
sessionId: String(s.sessionId),
|
||||
title: s.sessionName || '未命名会话',
|
||||
time: s.createdAt?.substring(0, 10) || '',
|
||||
}));
|
||||
const match = freshList.find((s: any) => s.sessionId === session.sessionId);
|
||||
if (match) {
|
||||
const idx = historyList.value.findIndex((h) => h.id === session.id);
|
||||
@@ -725,15 +802,17 @@ const runChat = async (sid: string, sessionId: string, message: string) => {
|
||||
sessionMessages.value.delete(session.id);
|
||||
sessionMessages.value.set(match.id, msgs || []);
|
||||
historyList.value[idx] = { ...match, status: session.status };
|
||||
saveChatCache(match.id, msgs || []);
|
||||
if (activeHistoryId.value === session.id) {
|
||||
activeHistoryId.value = match.id;
|
||||
}
|
||||
}
|
||||
loadSessionResults(match.id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 非虚拟会话:对话完成/失败后直接刷新会话结果
|
||||
loadSessionResults(sid);
|
||||
}
|
||||
saveChatCache(sid, sessionMessages.value.get(sid) || []);
|
||||
delete sendingSessions[sid];
|
||||
};
|
||||
|
||||
@@ -801,7 +880,6 @@ const runChat = async (sid: string, sessionId: string, message: string) => {
|
||||
if (ai && delta) {
|
||||
ai.loading = false;
|
||||
ai.content = (ai.content || '') + delta;
|
||||
saveChatCache(sid, list || []);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -812,7 +890,6 @@ const runChat = async (sid: string, sessionId: string, message: string) => {
|
||||
ai.loading = false;
|
||||
ai.content = answer;
|
||||
ai.thinkingSeconds = thinkStart ? Math.max(1, Math.round((Date.now() - thinkStart) / 1000)) : undefined;
|
||||
saveChatCache(sid, list || []);
|
||||
}
|
||||
finishChat(true);
|
||||
return;
|
||||
@@ -829,10 +906,12 @@ const runChat = async (sid: string, sessionId: string, message: string) => {
|
||||
finishChat(false, '对话连接失败,请重试');
|
||||
},
|
||||
onClose: () => {
|
||||
// 连接关闭兜底完成
|
||||
// 连接关闭兜底完成(主动停止时 handleStopGenerate 已把 AI 消息标记为「已停止生成」,
|
||||
// 此处走成功分支:flush 打字机剩余思考、清理会话状态,且不覆盖已有内容)
|
||||
if (!done) finishChat(!failed);
|
||||
},
|
||||
});
|
||||
currentWs.value = ws;
|
||||
if (!ws) {
|
||||
finishChat(false, 'WebSocket 初始化失败,请检查服务地址');
|
||||
}
|
||||
@@ -842,12 +921,15 @@ const handleSelectHistory = async (id: string) => {
|
||||
activeHistoryId.value = id;
|
||||
activeMenu.value = 'chat';
|
||||
const session = historyList.value.find((h) => h.id === id);
|
||||
// 加载会话内结果列表(session/get),用它判断会话类型:普通会话(无 workflow 结果)只依赖这一个接口,不再调 execution/get
|
||||
const results = await loadSessionResults(id);
|
||||
const hasWorkflow = Array.isArray(results) && results.some((r) => r.type === 'workflow');
|
||||
let asForm = false;
|
||||
if (session && !session.id.startsWith('virtual_')) {
|
||||
if (session && hasWorkflow) {
|
||||
// 工作流会话:查 execution/get 回显表单(此处逻辑暂保留,后续再优化为用 session/get 数据)
|
||||
try {
|
||||
const res = await getExecutionDetail(session.id);
|
||||
if (res.data) {
|
||||
// 工作流会话:回显表单页(可改参数重跑),并锁定工作流选择
|
||||
selectedWorkflowDetail.value = res.data;
|
||||
isHistoryWorkflow.value = true;
|
||||
asForm = true;
|
||||
@@ -863,11 +945,11 @@ const handleSelectHistory = async (id: string) => {
|
||||
}
|
||||
}
|
||||
if (!asForm) {
|
||||
// 普通对话会话 / 虚拟会话:切回对话页,加载消息记录
|
||||
// 普通对话会话 / 虚拟会话:切回对话页,把 session/get 结果转换为正常对话消息流展示
|
||||
selectedWorkflowDetail.value = null;
|
||||
isHistoryWorkflow.value = false;
|
||||
inputBarRef.value?.clearWorkflow?.();
|
||||
await loadSessionMessages(id);
|
||||
await loadSessionMessagesFromResults(id, results);
|
||||
}
|
||||
// 清除可重新执行的标记
|
||||
if (session?.status === 'completed' || session?.status === 'failed' || (session?.status === 'executing' && !sendingSessions[session.id])) {
|
||||
@@ -875,35 +957,58 @@ const handleSelectHistory = async (id: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 加载会话消息:优先后端接口,接口未就绪时读本地缓存兜底
|
||||
const loadSessionMessages = async (sid: string) => {
|
||||
const existing = sessionMessages.value.get(sid);
|
||||
if (existing && existing.length > 0) return; // 内存已有消息直接保留
|
||||
// 加载会话内结果(session/get):workflow+chat 混排,按时间倒序;返回结果列表供判断会话类型
|
||||
const loadSessionResults = async (sid: string) => {
|
||||
if (!sid) return [];
|
||||
sessionResultsLoading[sid] = true;
|
||||
try {
|
||||
const res: any = await getSessionMessages(sid);
|
||||
const list = Array.isArray(res?.data) ? res.data : res?.data?.list || res?.data?.messages;
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
const msgs: ChatMessage[] = list.map((m: any, i: number) => ({
|
||||
id: m.id || 'msg-' + Date.now() + '-' + i,
|
||||
content: m.content ?? m.message ?? m.reply ?? '',
|
||||
time: m.time || m.createDate || '',
|
||||
isUser: m.isUser === true || m.role === 'user',
|
||||
}));
|
||||
sessionMessages.value.set(sid, msgs);
|
||||
saveChatCache(sid, msgs);
|
||||
return;
|
||||
}
|
||||
const res: any = await getSessionResults(sid);
|
||||
const list = Array.isArray(res?.data?.list) ? res.data.list : Array.isArray(res?.data) ? res.data : [];
|
||||
sessionResultsMap[sid] = list;
|
||||
return list;
|
||||
} catch {
|
||||
// 接口不可用 → 兜底本地缓存
|
||||
}
|
||||
const cached = loadChatCache(sid);
|
||||
if (cached.length > 0) {
|
||||
sessionMessages.value.set(sid, cached);
|
||||
} else if (!sessionMessages.value.has(sid)) {
|
||||
sessionMessages.value.set(sid, []);
|
||||
if (!sessionResultsMap[sid]) sessionResultsMap[sid] = [];
|
||||
return sessionResultsMap[sid] || [];
|
||||
} finally {
|
||||
sessionResultsLoading[sid] = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 普通对话会话回显:把 session/get 的 chat 结果转换为「问题(用户) + 回复(AI)」消息流,正常对话气泡展示
|
||||
const loadSessionMessagesFromResults = async (sid: string, results: VOSessionInfoResult[]) => {
|
||||
const chats = (results || []).filter((r) => r.type === 'chat');
|
||||
const msgs: ChatMessage[] = [];
|
||||
const pending: { msg: ChatMessage; url: string }[] = [];
|
||||
// 结果按时间倒序 → 反转成正序,再按记录生成问答
|
||||
[...chats].reverse().forEach((r) => {
|
||||
const q = r.requestParams?.question;
|
||||
if (q) {
|
||||
msgs.push({ id: 'rq-' + r.id, content: String(q), time: r.createdAt || '', isUser: true });
|
||||
}
|
||||
if (r.resultFileUrl) {
|
||||
const ai: ChatMessage = { id: 'ra-' + r.id, content: '加载中…', time: r.createdAt || '', isUser: false, loading: true };
|
||||
msgs.push(ai);
|
||||
pending.push({ msg: ai, url: r.resultFileUrl });
|
||||
}
|
||||
});
|
||||
sessionMessages.value.set(sid, msgs);
|
||||
// 异步读取 txt 填充 AI 回复
|
||||
await Promise.all(
|
||||
pending.map(async ({ msg, url }) => {
|
||||
try {
|
||||
const res = await fetch(getUpFileUrl(url));
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
msg.content = await res.text();
|
||||
} catch {
|
||||
msg.content = '(回复加载失败)';
|
||||
} finally {
|
||||
msg.loading = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const createNewSession = () => {
|
||||
const sessionId = `virtual_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
||||
historyList.value.unshift({
|
||||
@@ -938,7 +1043,7 @@ const handleDeleteHistory = async (id: string) => {
|
||||
const isVirtual = id.startsWith('virtual_');
|
||||
if (!isVirtual) {
|
||||
try {
|
||||
await deleteExecutionSession({ id });
|
||||
await deleteSessionV2(id);
|
||||
} catch {
|
||||
// 后端已提示错误,继续本地删除
|
||||
}
|
||||
@@ -947,7 +1052,8 @@ const handleDeleteHistory = async (id: string) => {
|
||||
if (idx < 0) return;
|
||||
historyList.value.splice(idx, 1);
|
||||
sessionMessages.value.delete(id);
|
||||
clearChatCache(id);
|
||||
sessionResultsMap[id] = [];
|
||||
delete sessionResultsLoading[id];
|
||||
if (activeHistoryId.value === id) {
|
||||
if (historyList.value.length > 0) {
|
||||
activeHistoryId.value = historyList.value[0].id;
|
||||
|
||||
Reference in New Issue
Block a user