feat(ai): 重构AI聊天模块,使用agno框架替代langchain

- 移除langchain相关依赖,引入agno作为大模型开发框架
- 重构聊天会话和消息存储,使用agno的TeamSession管理
- 合并chat_message和chat_session模块为统一的chat模块
- 更新前端API和类型定义,适配新的后端接口
- 优化数据库日志字段类型,根据数据库类型自动选择
- 重构AI助手组件,使用新的聊天API
- 清理不再使用的模型、控制器和服务代码
This commit is contained in:
zhangtao
2026-03-13 00:41:44 +08:00
parent 8b83caf7fc
commit e03692467b
40 changed files with 2251 additions and 2650 deletions
+140
View File
@@ -0,0 +1,140 @@
import request from "@/utils/request";
const API_PATH = "/ai/chat";
export const AiChatAPI = {
getSessionList(query: {
page_no: number;
page_size: number;
title?: string;
created_at?: string[];
updated_at?: string[];
}) {
return request<ApiResponse<PageResult<ChatSession[]>>>({
url: `${API_PATH}/list`,
method: "get",
params: query,
});
},
createSession(body: { title: string }) {
return request<ApiResponse<ChatSession>>({
url: `${API_PATH}/create`,
method: "post",
data: body,
});
},
updateSession(id: string, body: { title: string }) {
return request<ApiResponse<ChatSession>>({
url: `${API_PATH}/update/${id}`,
method: "put",
data: body,
});
},
deleteSession(body: string[]) {
return request<ApiResponse>({
url: `${API_PATH}/delete`,
method: "delete",
data: body,
});
},
chat(body: { message: string; session_id?: string | null }) {
return request<ApiResponse<AiChatResponse>>({
url: `${API_PATH}/ai-chat`,
method: "post",
data: body,
});
},
getSessionDetail(sessionId: string) {
return request<ApiResponse<ChatSessionDetail>>({
url: `${API_PATH}/detail/${sessionId}`,
method: "get",
});
},
};
export default AiChatAPI;
export interface ChatSessionMessage {
id: string;
role: string;
content: string;
created_at: number | null;
}
export interface ChatSession {
session_id: string;
agent_id: string | null;
team_id: string | null;
team_name: string | null;
workflow_id: string | null;
user_id: string | null;
session_data: Record<string, any> | null;
agent_data: Record<string, any> | null;
team_data: Record<string, any> | null;
workflow_data: Record<string, any> | null;
metadata: Record<string, any> | null;
runs: Array<Record<string, any>> | null;
summary: Record<string, any> | null;
created_at: number | null;
updated_at: number | null;
id: string;
title: string | null;
created_time: string | null;
updated_time: string | null;
message_count: number;
messages: ChatSessionMessage[];
}
export interface SessionGroup {
id: string;
title: string;
sessions: ChatSession[];
}
export interface UserInfo {
id: number;
name: string;
username: string;
avatar: string;
email: string;
}
export interface AiChatResponse {
response: string;
session_id: string;
function_calls: Array<{
name: string;
arguments: Record<string, any>;
}> | null;
}
export interface ChatSessionDetail {
session_id: string;
agent_id: string | null;
team_id: string | null;
team_name: string | null;
workflow_id: string | null;
user_id: string | null;
session_data: Record<string, any> | null;
agent_data: Record<string, any> | null;
team_data: Record<string, any> | null;
workflow_data: Record<string, any> | null;
metadata: Record<string, any> | null;
runs: Array<Record<string, any>> | null;
summary: Record<string, any> | null;
created_at: number | null;
updated_at: number | null;
id: string;
title: string | null;
created_time: string | null;
updated_time: string | null;
message_count: number;
messages: ChatSessionMessage[];
}
@@ -1,97 +0,0 @@
import request from "@/utils/request";
const API_PATH = "/ai/chat_message";
const AiChatMessageAPI = {
listMessage(query?: MessagePageQuery) {
return request<ApiResponse<PageResult<MessageTable[]>>>({
url: `${API_PATH}/list`,
method: "get",
params: query,
});
},
detailMessage(id: number) {
return request<ApiResponse<MessageTable>>({
url: `${API_PATH}/detail/${id}`,
method: "get",
});
},
getMessagesBySession(sessionId: number, query?: PageQuery) {
return request<ApiResponse<PageResult<MessageTable[]>>>({
url: `${API_PATH}/session/${sessionId}`,
method: "get",
params: query,
});
},
createMessage(body: MessageForm) {
return request<ApiResponse<MessageTable>>({
url: `${API_PATH}/create`,
method: "post",
data: body,
});
},
updateMessage(id: number, body: MessageForm) {
return request<ApiResponse<MessageTable>>({
url: `${API_PATH}/update/${id}`,
method: "put",
data: body,
});
},
deleteMessage(body: number[]) {
return request<ApiResponse>({
url: `${API_PATH}/delete`,
method: "delete",
data: body,
});
},
};
export default AiChatMessageAPI;
export interface MessagePageQuery extends PageQuery {
session_id?: number;
type?: string;
content?: string;
created_time?: string[];
updated_time?: string[];
}
export interface MessageTable extends BaseType {
session_id: number;
type: "user" | "assistant";
content: string;
timestamp: number;
files?: Record<string, any>[];
}
export interface MessageForm extends BaseFormType {
session_id?: number;
type?: "user" | "assistant";
content?: string;
timestamp?: number;
files?: Record<string, any>[];
}
export interface ChatMessage {
id: string;
type: "user" | "assistant";
content: string;
timestamp: number;
loading?: boolean;
collapsed?: boolean;
files?: UploadedFile[];
}
export interface UploadedFile {
id: string;
name: string;
size: number;
type: string;
url?: string;
file?: File;
}
@@ -1,85 +0,0 @@
import request from "@/utils/request";
const API_PATH = "/ai/chat_session";
const AiChatSessionAPI = {
listSession(query?: SessionPageQuery) {
return request<ApiResponse<PageResult<SessionTable[]>>>({
url: `${API_PATH}/list`,
method: "get",
params: query,
});
},
detailSession(id: number) {
return request<ApiResponse<SessionTable>>({
url: `${API_PATH}/detail/${id}`,
method: "get",
});
},
createSession(body: SessionForm) {
return request<ApiResponse<SessionTable>>({
url: `${API_PATH}/create`,
method: "post",
data: body,
});
},
updateSession(id: number, body: SessionForm) {
return request<ApiResponse<SessionTable>>({
url: `${API_PATH}/update/${id}`,
method: "put",
data: body,
});
},
deleteSession(body: number[]) {
return request<ApiResponse>({
url: `${API_PATH}/delete`,
method: "delete",
data: body,
});
},
};
export default AiChatSessionAPI;
export interface SessionPageQuery extends PageQuery {
title?: string;
status?: string;
created_time?: string[];
updated_time?: string[];
}
export interface SessionTable extends BaseType {
title: string;
created_by?: CommonType;
updated_by?: CommonType;
}
export interface SessionForm extends BaseFormType {
title?: string;
}
export interface ChatSession {
id: number;
title: string;
created_time: string;
updated_time: string;
message_count?: number;
created_by?: CommonType;
updated_by?: CommonType;
}
export interface SessionGroup {
title: string;
sessions: ChatSession[];
}
export interface UserInfo {
id: number;
name: string;
username: string;
avatar?: string;
}
@@ -1,26 +0,0 @@
import request from "@/utils/request";
const API_PATH = "/application/ai";
export const McpAPI = {
/**
* 查询应用列表
* @param query 查询参数
*/
chatMcp(query: AiChatQuery) {
return request<ApiResponse>({
url: `${API_PATH}/chat`,
method: "post",
params: query,
});
},
};
export default McpAPI;
/**
* 应用表单
*/
export interface AiChatQuery {
message: string;
}
+58 -6
View File
@@ -119,11 +119,11 @@
</template>
<script setup lang="ts">
import { nextTick, onBeforeUnmount, onMounted, watch } from "vue";
import { nextTick, onBeforeUnmount, onMounted, watch, ref, computed } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { useSettingsStore } from "@/store";
import McpAPI from "@/api/module_application/mcp";
import { AiChatAPI, ChatSession, ChatSessionDetail } from "@/api/module_ai/chat";
type ToolFunctionCall = {
name: string;
@@ -165,6 +165,12 @@ const command = ref("");
const loading = ref(false);
const response = ref<AiResponse | null>(null);
// 会话管理
const sessions = ref<ChatSession[]>([]);
const currentSessionId = ref<string | null>(null);
const currentSession = ref<ChatSessionDetail | null>(null);
const sessionLoading = ref(false);
const fabCollapsed = useStorage<boolean>("vea:ui:ai_assistant_fab_collapsed", false);
const fabRight = ref(30);
@@ -300,11 +306,46 @@ const examples = [
"获取姓名为张三的用户信息",
];
// 加载会话列表
const loadSessions = async () => {
try {
sessionLoading.value = true;
const response = await AiChatAPI.getSessionList({ page_no: 1, page_size: 100 });
if (response.data?.success) {
sessions.value = response.data.data?.items || [];
}
} catch (error) {
console.error("加载会话列表失败:", error);
} finally {
sessionLoading.value = false;
}
};
// 加载会话详情
const loadSessionDetail = async (sessionId: string) => {
try {
sessionLoading.value = true;
const response = await AiChatAPI.getSessionDetail(sessionId);
if (response.data?.success) {
currentSessionId.value = sessionId;
currentSession.value = response.data.data;
} else {
ElMessage.error("获取会话详情失败");
}
} catch (error) {
console.error("获取会话详情失败:", error);
ElMessage.error("获取会话详情失败");
} finally {
sessionLoading.value = false;
}
};
// 打开对话框
const handleOpen = () => {
dialogVisible.value = true;
command.value = "";
response.value = null;
loadSessions();
};
// 关闭对话框
@@ -334,8 +375,9 @@ const handleExecute = async () => {
try {
// 调用 AI API 解析命令
const result = await McpAPI.chatMcp({
const result = await AiChatAPI.chat({
message: rawCommand,
session_id: currentSessionId.value || undefined,
});
if (!result.data.success) {
@@ -343,10 +385,18 @@ const handleExecute = async () => {
return;
}
// 更新会话ID
if (result.data.data?.session_id) {
currentSessionId.value = result.data.data.session_id;
// 重新加载会话列表和详情
await loadSessions();
await loadSessionDetail(result.data.data.session_id);
}
// 解析 AI 返回的操作类型
const action = parseAction(result.data, rawCommand);
const action = parseAction(result.data.data, rawCommand);
response.value = {
explanation: result.data.msg ?? "命令解析成功,准备执行操作",
explanation: result.data.data?.response ?? "命令解析成功,准备执行操作",
action,
};
@@ -475,7 +525,9 @@ const tryDirectNavigate = (rawCommand: string): AiResponse | null => {
// 解析 AI 返回的操作类型
const parseAction = (result: any, rawCommand: string): AiAction | null => {
const cmd = normalizeText(rawCommand);
const primaryCall = result.functionCalls?.[0];
// 后端返回的是 function_calls(下划线命名)
const functionCalls = result.function_calls || result.functionCalls;
const primaryCall = functionCalls?.[0];
const functionName = primaryCall?.name;
// 优先从函数名推断路由,其次从命令文本匹配
@@ -60,7 +60,7 @@
import { ref, computed } from "vue";
import { Promotion, Paperclip, Document, Close } from "@element-plus/icons-vue";
import type { UploadFile } from "element-plus";
import type { UploadedFile } from "@/api/module_ai/chat_message";
import type { UploadedFile } from "@/views/module_ai/chat/types";
interface Props {
disabled?: boolean;
@@ -19,7 +19,7 @@
import { ref, nextTick, watch } from "vue";
import WelcomeScreen from "./WelcomeScreen.vue";
import MessageItem from "./MessageItem.vue";
import type { ChatMessage } from "@/api/module_ai/chat_message";
import type { ChatMessage } from "@/views/module_ai/chat/types";
interface Props {
messages: ChatMessage[];
@@ -77,7 +77,7 @@ import hljs from "highlight.js";
import DOMPurify from "dompurify";
import "highlight.js/styles/atom-one-light.css";
import { useUserStoreHook } from "@/store";
import type { ChatMessage } from "@/api/module_ai/chat_message";
import type { ChatMessage } from "@/views/module_ai/chat/types";
interface Props {
message: ChatMessage;
@@ -46,7 +46,9 @@
@click="handleSelectSession(session)"
>
<el-icon class="session-icon"><ChatLineRound /></el-icon>
<span class="session-title">{{ session.title }}</span>
<span class="session-title">
{{ session.title || session.session_data?.session_name || "未命名会话" }}
</span>
<el-dropdown
trigger="click"
@command="(cmd) => handleSessionCommand(cmd, session)"
@@ -122,10 +124,11 @@ import {
ArrowDown,
} from "@element-plus/icons-vue";
import { useUserStoreHook } from "@/store";
import AiAPI, { ChatSession, SessionGroup, UserInfo } from "@/api/module_ai/chat_session";
import { ChatSession, SessionGroup, UserInfo } from "@/api/module_ai/chat";
import AiChatAPI from "@/api/module_ai/chat";
interface Props {
currentSessionId?: number | null;
currentSessionId?: string | null;
isCollapsed?: boolean;
}
@@ -157,7 +160,9 @@ const filteredSessions = computed<ChatSession[]>(() => {
return sessions.value;
}
const query = searchQuery.value.toLowerCase();
return sessions.value.filter((session) => session.title.toLowerCase().includes(query));
return sessions.value.filter((session: ChatSession) =>
(session.title || "").toLowerCase().includes(query)
);
});
const groupedSessions = computed<SessionGroup[]>(() => {
@@ -173,7 +178,9 @@ const groupedSessions = computed<SessionGroup[]>(() => {
const earlierSessions: ChatSession[] = [];
filteredSessions.value.forEach((session) => {
const updatedTime = new Date(session.updated_time).getTime();
if (!session.updated_at) return;
// Unix 时间戳(秒)转换为毫秒
const updatedTime = session.updated_at * 1000;
if (updatedTime >= todayStart) {
todaySessions.push(session);
@@ -189,19 +196,19 @@ const groupedSessions = computed<SessionGroup[]>(() => {
const groups: SessionGroup[] = [];
if (todaySessions.length > 0) {
groups.push({ title: "今天", sessions: todaySessions });
groups.push({ id: "today", title: "今天", sessions: todaySessions });
}
if (yesterdaySessions.length > 0) {
groups.push({ title: "昨天", sessions: yesterdaySessions });
groups.push({ id: "yesterday", title: "昨天", sessions: yesterdaySessions });
}
if (weekSessions.length > 0) {
groups.push({ title: "本周", sessions: weekSessions });
groups.push({ id: "week", title: "本周", sessions: weekSessions });
}
if (earlierSessions.length > 0) {
groups.push({ title: "更早", sessions: earlierSessions });
groups.push({ id: "earlier", title: "更早", sessions: earlierSessions });
}
return groups;
@@ -236,7 +243,7 @@ const handleSessionCommand = async (command: string, session: ChatSession) => {
inputPattern: /.+/,
inputErrorMessage: "会话名称不能为空",
});
await AiAPI.updateSession(session.id, { title: value });
await AiChatAPI.updateSession(session.id, { title: value });
session.title = value;
ElMessage.success("重命名成功");
} catch (error) {
@@ -253,8 +260,8 @@ const handleSessionCommand = async (command: string, session: ChatSession) => {
cancelButtonText: "取消",
type: "warning",
});
await AiAPI.deleteSession([session.id]);
const index = sessions.value.findIndex((s) => s.id === session.id);
await AiChatAPI.deleteSession([session.id]);
const index = sessions.value.findIndex((s: ChatSession) => s.id === session.id);
if (index > -1) {
sessions.value.splice(index, 1);
}
@@ -282,16 +289,36 @@ const handleUserCommand = (command: string) => {
const loadSessions = async () => {
try {
const res = await AiAPI.listSession({ page_no: 1, page_size: 100 });
if (res.data?.code === 0 || res.data?.success === true) {
sessions.value = (res.data.data?.items || [])
.filter((item: any) => item.id !== undefined)
const res = await AiChatAPI.getSessionList({ page_no: 1, page_size: 100 });
const responseData = res.data;
const data = responseData?.data;
if (data?.items && Array.isArray(data.items)) {
sessions.value = data.items
.filter((item: any) => item.session_id !== undefined)
.map((item: any) => ({
id: item.id,
title: item.title,
created_time: item.created_time,
updated_time: item.updated_time,
message_count: 0,
id: item.session_id,
title: item.session_data?.session_name || item.session_id?.slice(0, 8) || "新会话",
created_at: item.created_at,
updated_at: item.updated_at,
message_count: item.runs?.length || 0,
session_id: item.session_id,
session_type: item.session_type,
agent_id: item.agent_id,
user_id: item.user_id,
team_id: item.team_id,
team_name: item.team_name,
workflow_id: item.workflow_id,
summary: item.summary,
metadata: item.metadata,
runs: item.runs,
session_data: item.session_data,
agent_data: item.agent_data,
team_data: item.team_data,
workflow_data: item.workflow_data,
created_time: item.created_at ? new Date(item.created_at * 1000).toISOString() : null,
updated_time: item.updated_at ? new Date(item.updated_at * 1000).toISOString() : null,
messages: item.runs?.flatMap((run: any) => run.messages || []) || [],
}));
}
} catch (error) {
+124 -143
View File
@@ -27,7 +27,7 @@
ref="chatMessagesRef"
:messages="messages"
:error="error"
@prompt-click="handlePromptClick"
@prompt-click="handleSendMessage"
@error-close="error = ''"
/>
</el-main>
@@ -49,75 +49,69 @@ defineOptions({
name: "Chat",
inheritAttrs: false,
});
import { ref, onMounted, onUnmounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import ChatNavbar from "./components/ChatNavbar.vue";
import ChatMessages from "./components/ChatMessages.vue";
import ChatInput from "./components/ChatInput.vue";
import Sidebar from "./components/Sidebar.vue";
import AiChatSessionAPI, { ChatSession } from "@/api/module_ai/chat_session";
import AiChatMessageAPI, { ChatMessage, UploadedFile } from "@/api/module_ai/chat_message";
import AiChatAPI, { ChatSession } from "@/api/module_ai/chat";
import { Auth } from "@/utils/auth";
import type { ChatMessage, UploadedFile } from "./types";
// 状态
const messages = ref<ChatMessage[]>([]);
const sending = ref(false);
const isConnected = ref(false);
const connectionStatus = ref<"connected" | "connecting" | "disconnected">("disconnected");
const error = ref<string>("");
const chatMessagesRef = ref<{ scrollToBottom: () => void }>();
const sidebarRef = ref<{ loadSessions: () => void }>();
const currentSessionId = ref<number | null>(null);
const error = ref("");
const currentSessionId = ref<string | null>(null);
const isSidebarCollapsed = ref(false);
// Refs
const chatMessagesRef = ref<{ scrollToBottom: () => void }>();
const sidebarRef = ref<{ loadSessions: () => void }>();
// WebSocket
let ws: WebSocket | null = null;
const WS_URL = import.meta.env.VITE_APP_WS_ENDPOINT;
// ============ WebSocket 操作 ============
const connectWebSocket = () => {
if (ws?.readyState === WebSocket.OPEN) {
return;
}
if (ws?.readyState === WebSocket.OPEN) return;
connectionStatus.value = "connecting";
error.value = "";
try {
// WebSocket连接不支持直接设置请求头
// 这里使用URL参数传递token,后端需要相应处理
const url = new URL("/api/v1/ai/chat/ws", WS_URL);
const token = Auth.getAccessToken();
if (token) {
url.searchParams.append("token", token);
}
if (token) url.searchParams.append("token", token);
ws = new WebSocket(url.toString());
ws.onopen = () => {
console.log("WebSocket 连接已建立");
isConnected.value = true;
connectionStatus.value = "connected";
ElMessage.success("连接成功");
};
ws.onmessage = (event) => {
handleWebSocketMessage({ content: event.data });
};
ws.onmessage = (event) => handleWebSocketMessage(event.data);
ws.onclose = (event) => {
console.log("WebSocket 连接已关闭", event.code, event.reason);
ws.onclose = () => {
isConnected.value = false;
connectionStatus.value = "disconnected";
finishLoadingMessages();
};
ws.onerror = (err) => {
console.error("WebSocket 错误:", err);
ws.onerror = () => {
isConnected.value = false;
connectionStatus.value = "disconnected";
ElMessage.error("连接失败,请检查服务器状态");
finishLoadingMessages();
};
} catch (err) {
console.error("创建 WebSocket 连接失败:", err);
} catch {
connectionStatus.value = "disconnected";
error.value = "无法创建连接";
}
@@ -142,78 +136,85 @@ const toggleConnection = () => {
}
};
const handleWebSocketMessage = (data: any) => {
// ============ 消息处理 ============
const handleWebSocketMessage = (data: string) => {
const lastMessage = messages.value[messages.value.length - 1];
const content = data || "";
if (lastMessage && lastMessage.type === "assistant" && lastMessage.loading) {
lastMessage.content += data.content || data.message || "";
if (lastMessage?.type === "assistant" && lastMessage.loading) {
lastMessage.content += content;
} else {
addMessage("assistant", data.content || data.message || "收到回复");
addMessage("assistant", content);
}
chatMessagesRef.value?.scrollToBottom();
};
const handleSendMessage = async (message: string, files?: UploadedFile[]) => {
if ((!message && !files) || !isConnected.value || sending.value) {
return;
}
const addMessage = (type: "user" | "assistant", content: string, files?: UploadedFile[]) => {
messages.value.push({
id: generateId(),
type,
content,
timestamp: Date.now(),
collapsed: content.length > 200,
files,
});
};
const lastMessage = messages.value[messages.value.length - 1];
if (lastMessage && lastMessage.type === "assistant" && lastMessage.loading) {
lastMessage.loading = false;
}
// 如果没有当前会话,先创建会话
if (!currentSessionId.value) {
try {
// 使用第一条消息的前20个字符作为会话标题
const title = message.substring(0, 20) + (message.length > 20 ? "..." : "");
const res = await AiChatSessionAPI.createSession({ title });
if (res.data?.code === 0 || res.data?.success === true) {
currentSessionId.value = res.data.data.id ?? null;
sidebarRef.value?.loadSessions();
} else {
throw new Error("创建会话失败");
}
} catch (err) {
console.error("创建会话失败:", err);
ElMessage.error("创建会话失败,请重试");
return;
const finishLoadingMessages = () => {
messages.value.forEach((msg) => {
if (msg.type === "assistant" && msg.loading) {
msg.loading = false;
msg.collapsed = msg.content.length > 200;
}
});
};
const generateId = () => {
return Date.now().toString(36) + Math.random().toString(36).slice(2);
};
// ============ 发送消息 ============
const handleSendMessage = async (message: string, files?: UploadedFile[]) => {
if ((!message && !files) || !isConnected.value || sending.value) return;
// 结束上一个加载中的消息
finishLoadingMessages();
// 创建新会话(如果没有)
if (!currentSessionId.value) {
const success = await createNewSession(message);
if (!success) return;
}
// 添加用户消息
addMessage("user", message, files);
const loadingMessage: ChatMessage = {
// 添加加载中的助手消息
messages.value.push({
id: generateId(),
type: "assistant",
content: "",
timestamp: Date.now(),
loading: true,
};
messages.value.push(loadingMessage);
});
sending.value = true;
chatMessagesRef.value?.scrollToBottom();
try {
if (ws?.readyState === WebSocket.OPEN) {
const payload = {
message,
session_id: currentSessionId.value,
files: files?.map((f) => ({
name: f.name,
type: f.type,
size: f.size,
})),
};
ws.send(JSON.stringify(payload));
ws.send(
JSON.stringify({
message,
session_id: currentSessionId.value,
files: files?.map((f) => ({ name: f.name, type: f.type, size: f.size })),
})
);
} else {
throw new Error("WebSocket 连接未建立");
}
} catch (err) {
console.error("发送消息失败:", err);
} catch {
messages.value.pop();
error.value = "发送消息失败,请检查连接状态";
ElMessage.error("发送失败");
@@ -222,16 +223,57 @@ const handleSendMessage = async (message: string, files?: UploadedFile[]) => {
}
};
const addMessage = (type: "user" | "assistant", content: string, files?: UploadedFile[]) => {
const message: ChatMessage = {
id: generateId(),
type,
content,
timestamp: Date.now(),
collapsed: content.length > 200,
files,
};
messages.value.push(message);
const createNewSession = async (firstMessage: string): Promise<boolean> => {
try {
const title = firstMessage.slice(0, 20) + (firstMessage.length > 20 ? "..." : "");
const res = await AiChatAPI.createSession({ title });
if (res.data?.code === 0 || res.data?.success) {
currentSessionId.value = res.data.data?.id ?? null;
sidebarRef.value?.loadSessions();
return true;
}
throw new Error("创建会话失败");
} catch {
ElMessage.error("创建会话失败,请重试");
return false;
}
};
// ============ 会话操作 ============
const handleSelectSession = async (session: ChatSession) => {
currentSessionId.value = session.id;
messages.value = [];
try {
const response = await AiChatAPI.getSessionDetail(session.id);
if (response.data?.code !== 0) {
ElMessage.error("获取会话详情失败");
return;
}
const sessionData = response.data.data || {};
const runs = sessionData.runs || [];
runs.forEach((run: any) => {
const runMessages = run.messages || [];
runMessages.forEach((msg: any) => {
if (msg.role === "user" || msg.role === "assistant") {
addMessage(msg.role, msg.content);
}
});
});
ElMessage.success(`已切换到会话:${session.title}`);
} catch {
ElMessage.error("获取会话详情失败");
}
};
const handleNewSession = () => {
currentSessionId.value = null;
messages.value = [];
ElMessage.success("已开启新对话");
};
const handleClearChat = async () => {
@@ -248,74 +290,13 @@ const handleClearChat = async () => {
}
};
const handlePromptClick = (prompt: string) => {
handleSendMessage(prompt);
};
const finishLoadingMessages = () => {
messages.value.forEach((message) => {
if (message.type === "assistant" && message.loading) {
message.loading = false;
message.collapsed = message.content.length > 200;
}
});
};
const generateId = () => {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
};
const handleSelectSession = async (session: ChatSession) => {
currentSessionId.value = session.id;
messages.value = [];
await loadMessages(session.id);
ElMessage.success(`已切换到会话:${session.title}`);
};
const handleNewSession = async () => {
currentSessionId.value = null;
messages.value = [];
ElMessage.success("已开启新对话");
};
const loadMessages = async (sessionId: number) => {
try {
const res = await AiChatMessageAPI.getMessagesBySession(sessionId, {
page_no: 1,
page_size: 100,
});
if (res.data?.code === 0 || res.data?.success === true) {
const apiMessages = res.data.data?.items || [];
messages.value = apiMessages.map((msg: any) => ({
id: msg.id.toString(),
type: msg.type as "user" | "assistant",
content: msg.content,
timestamp: msg.timestamp,
collapsed: msg.content.length > 200,
files: msg.files?.map((f: any) => ({
id: generateId(),
name: f.name || "",
size: f.size || 0,
type: f.type || "",
})),
}));
}
} catch (error) {
console.error("加载消息失败:", error);
}
};
const toggleSidebar = () => {
isSidebarCollapsed.value = !isSidebarCollapsed.value;
};
onMounted(() => {
connectWebSocket();
});
onUnmounted(() => {
disconnectWebSocket();
});
// ============ 生命周期 ============
onMounted(connectWebSocket);
onUnmounted(disconnectWebSocket);
</script>
<style lang="scss" scoped>
@@ -0,0 +1,38 @@
// 聊天消息类型
export interface ChatMessage {
id: string;
type: "user" | "assistant";
content: string;
timestamp: number;
loading?: boolean;
collapsed?: boolean;
files?: UploadedFile[];
}
// 上传文件类型
export interface UploadedFile {
id: string;
name: string;
size: number;
type: string;
url?: string;
file?: File;
}
// 会话消息类型(来自后端)
export interface SessionMessage {
id: string;
role: "user" | "assistant";
content: string;
created_time?: string;
}
// 会话详情类型
export interface SessionDetail {
id: string;
title: string;
created_time?: string;
updated_time?: string;
message_count: number;
messages: SessionMessage[];
}
@@ -1,604 +0,0 @@
<!-- AI智能助手消息列表 -->
<template>
<div class="app-container">
<!-- 内容区域 -->
<el-card class="data-table">
<template #header>
<div class="card-header">
<span>
对话消息列表
<el-tooltip content="对话消息列表">
<QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip>
</span>
</div>
<!-- 搜索区域 -->
<div v-show="visible" class="search-container">
<el-form
ref="queryFormRef"
:model="queryFormData"
label-suffix=":"
:inline="true"
@submit.prevent="handleQuery"
>
<el-form-item prop="session_id" label="会话ID">
<el-input v-model="queryFormData.session_id" placeholder="请输入会话ID" clearable />
</el-form-item>
<el-form-item prop="type" label="类型">
<el-select
v-model="queryFormData.type"
placeholder="请选择类型"
style="width: 170px"
clearable
>
<el-option value="user" label="用户" />
<el-option value="assistant" label="助手" />
</el-select>
</el-form-item>
<el-form-item prop="content" label="内容">
<el-input v-model="queryFormData.content" placeholder="请输入内容" clearable />
</el-form-item>
<el-form-item v-if="isExpand" prop="created_time" label="创建时间">
<DatePicker
v-model="createdDateRange"
@update:model-value="handleCreatedDateRangeChange"
/>
</el-form-item>
<!-- 查询、重置、展开/收起按钮 -->
<el-form-item>
<el-button
v-hasPerm="['module_example:demo:query']"
type="primary"
icon="search"
@click="handleQuery"
>
查询
</el-button>
<el-button
v-hasPerm="['module_example:demo:query']"
icon="refresh"
@click="handleResetQuery"
>
重置
</el-button>
<!-- 展开/收起 -->
<template v-if="isExpandable">
<el-link
class="ml-3"
type="primary"
underline="never"
@click="isExpand = !isExpand"
>
{{ isExpand ? "收起" : "展开" }}
<el-icon>
<template v-if="isExpand">
<ArrowUp />
</template>
<template v-else>
<ArrowDown />
</template>
</el-icon>
</el-link>
</template>
</el-form-item>
</el-form>
</div>
</template>
<!-- 功能区域 -->
<div class="data-table__toolbar">
<div class="data-table__toolbar--left">
<el-row :gutter="10">
<el-col :span="1.5">
<el-button
v-hasPerm="['module_ai:chat_message:create']"
type="success"
icon="plus"
@click="handleOpenDialog('create')"
>
新增
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
v-hasPerm="['module_ai:chat_message:delete']"
type="danger"
icon="delete"
:disabled="selectIds.length === 0"
@click="handleDelete(selectIds)"
>
批量删除
</el-button>
</el-col>
</el-row>
</div>
<div class="data-table__toolbar--right">
<el-row :gutter="10">
<el-col :span="1.5">
<el-tooltip content="搜索显示/隐藏">
<el-button
v-hasPerm="['*:*:*']"
type="info"
icon="search"
circle
@click="visible = !visible"
/>
</el-tooltip>
</el-col>
<el-col :span="1.5">
<el-tooltip content="刷新">
<el-button
v-hasPerm="['module_ai:chat_message:query']"
type="primary"
icon="refresh"
circle
@click="handleRefresh"
/>
</el-tooltip>
</el-col>
</el-row>
</div>
</div>
<!-- 表格区域:系统配置列表 -->
<el-table
ref="tableRef"
v-loading="loading"
:data="pageTableData"
highlight-current-row
class="data-table__content"
height="calc(100vh - 440px)"
max-height="calc(100vh - 440px)"
border
stripe
@selection-change="handleSelectionChange"
>
<template #empty>
<el-empty :image-size="80" description="暂无数据" />
</template>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'selection')?.show"
type="selection"
min-width="55"
align="center"
/>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'index')?.show"
fixed
label="序号"
min-width="60"
>
<template #default="scope">
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'session_id')?.show"
label="会话ID"
prop="session_id"
min-width="120"
/>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'type')?.show"
label="类型"
prop="type"
min-width="100"
>
<template #default="scope">
<el-tag :type="scope.row.type === 'user' ? 'primary' : 'success'">
{{ scope.row.type === "user" ? "用户" : "助手" }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'content')?.show"
label="内容"
prop="content"
min-width="200"
show-overflow-tooltip
>
<template #default="scope">
<MarkdownRenderer :content="scope.row.content" :max-length="100" />
</template>
</el-table-column>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'timestamp')?.show"
label="时间戳"
prop="timestamp"
min-width="180"
/>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'created_time')?.show"
label="创建时间"
prop="created_time"
min-width="180"
/>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'updated_time')?.show"
label="更新时间"
prop="updated_time"
min-width="180"
/>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'operation')?.show"
fixed="right"
label="操作"
align="center"
min-width="180"
>
<template #default="scope">
<el-button
v-hasPerm="['module_example:demo:detail']"
type="info"
size="small"
link
icon="document"
@click="handleOpenDialog('detail', scope.row.id)"
>
详情
</el-button>
<el-button
v-hasPerm="['module_example:demo:update']"
type="primary"
size="small"
link
icon="edit"
@click="handleOpenDialog('update', scope.row.id)"
>
编辑
</el-button>
<el-button
v-hasPerm="['module_example:demo:delete']"
type="danger"
size="small"
link
icon="delete"
@click="handleDelete([scope.row.id])"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<template #footer>
<pagination
v-model:total="total"
v-model:page="queryFormData.page_no"
v-model:limit="queryFormData.page_size"
@pagination="loadingData"
/>
</template>
</el-card>
<!-- 弹窗区域 -->
<el-dialog
v-model="dialogVisible.visible"
:title="dialogVisible.title"
@close="handleCloseDialog"
>
<!-- 详情 -->
<template v-if="dialogVisible.type === 'detail'">
<el-descriptions :column="4" border>
<el-descriptions-item label="会话ID" :span="2">
{{ detailFormData.session_id }}
</el-descriptions-item>
<el-descriptions-item label="类型" :span="2">
<el-tag :type="detailFormData.type === 'user' ? 'primary' : 'success'">
{{ detailFormData.type === "user" ? "用户" : "助手" }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="内容" :span="4">
<div v-if="detailFormData.content">
<MarkdownRenderer :content="detailFormData.content" />
</div>
<div v-else class="empty-content">暂无内容</div>
</el-descriptions-item>
<el-descriptions-item label="时间戳" :span="2">
{{ detailFormData.timestamp }}
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">
{{ detailFormData.created_time }}
</el-descriptions-item>
<el-descriptions-item label="更新时间" :span="2">
{{ detailFormData.updated_time }}
</el-descriptions-item>
</el-descriptions>
</template>
<!-- 新增、编辑表单 -->
<template v-else>
<el-form
ref="dataFormRef"
:model="formData"
:rules="rules"
label-suffix=":"
label-width="auto"
label-position="right"
inline
>
<el-form-item label="会话ID" prop="session_id">
<el-input-number v-model="formData.session_id" placeholder="请输入会话ID" />
</el-form-item>
<el-form-item label="类型" prop="type">
<el-radio-group v-model="formData.type">
<el-radio value="user">用户</el-radio>
<el-radio value="assistant">助手</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="内容" prop="content">
<WangEditor v-model="formData.content" height="200px" />
</el-form-item>
<el-form-item label="时间戳" prop="timestamp">
<el-input-number v-model="formData.timestamp" placeholder="请输入时间戳" />
</el-form-item>
</el-form>
</template>
<template #footer>
<div class="dialog-footer">
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
<el-button @click="handleCloseDialog">取消</el-button>
<el-button v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">
确定
</el-button>
<el-button v-else type="primary" @click="handleCloseDialog">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: "ChatMessage",
inheritAttrs: false,
});
import { ref, reactive, onMounted } from "vue";
import { ElMessageBox } from "element-plus";
import AiChatMessageAPI, {
MessageTable,
MessageForm,
MessagePageQuery,
} from "@/api/module_ai/chat_message";
import { QuestionFilled, ArrowUp, ArrowDown } from "@element-plus/icons-vue";
import { formatToDateTime } from "@/utils/dateUtil";
import MarkdownRenderer from "@/components/MarkdownRenderer/index.vue";
import WangEditor from "@/components/WangEditor/index.vue";
const visible = ref(true);
const queryFormRef = ref();
const dataFormRef = ref();
const total = ref(0);
const selectIds = ref<number[]>([]);
const selectionRows = ref<MessageTable[]>([]);
const loading = ref(false);
const isExpand = ref(false);
const isExpandable = ref(true);
// 日期范围临时变量
const createdDateRange = ref<[Date, Date] | []>([]);
// 处理创建时间范围变化
function handleCreatedDateRangeChange(range: [Date, Date]) {
createdDateRange.value = range;
if (range && range.length === 2) {
queryFormData.created_time = [formatToDateTime(range[0]), formatToDateTime(range[1])];
} else {
queryFormData.created_time = undefined;
}
}
// 分页表单
const pageTableData = ref<MessageTable[]>([]);
// 表格列配置
const tableColumns = ref([
{ prop: "selection", label: "选择框", show: true },
{ prop: "index", label: "序号", show: true },
{ prop: "session_id", label: "会话ID", show: true },
{ prop: "type", label: "类型", show: true },
{ prop: "content", label: "内容", show: true },
{ prop: "timestamp", label: "时间戳", show: true },
{ prop: "created_time", label: "创建时间", show: true },
{ prop: "updated_time", label: "更新时间", show: true },
{ prop: "operation", label: "操作", show: true },
]);
// 详情表单
const detailFormData = ref<MessageTable>({
session_id: 0,
type: "user",
content: "",
timestamp: 0,
});
// 分页查询参数
const queryFormData = reactive<MessagePageQuery>({
page_no: 1,
page_size: 10,
session_id: undefined,
type: undefined,
content: undefined,
created_time: undefined,
});
// 编辑表单
const formData = reactive<MessageForm>({
id: undefined,
session_id: undefined,
type: "user",
content: "",
timestamp: undefined,
});
// 弹窗状态
const dialogVisible = reactive({
title: "",
visible: false,
type: "create" as "create" | "update" | "detail",
});
// 表单验证规则
const rules = reactive({
session_id: [{ required: true, message: "请输入会话ID", trigger: "blur" }],
type: [{ required: true, message: "请选择类型", trigger: "change" }],
content: [{ required: true, message: "请输入内容", trigger: "blur" }],
});
// 列表刷新
async function handleRefresh() {
await loadingData();
}
// 加载表格数据
async function loadingData() {
loading.value = true;
try {
const response = await AiChatMessageAPI.listMessage(queryFormData);
pageTableData.value = response.data.data.items;
total.value = response.data.data.total;
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
}
// 查询(重置页码后获取数据)
async function handleQuery() {
queryFormData.page_no = 1;
loadingData();
}
// 重置查询
async function handleResetQuery() {
queryFormRef.value.resetFields();
queryFormData.page_no = 1;
loadingData();
}
// 定义初始表单数据常量
const initialFormData: MessageForm = {
id: undefined,
session_id: undefined,
type: "user",
content: "",
timestamp: undefined,
};
// 重置表单
async function resetForm() {
if (dataFormRef.value) {
dataFormRef.value.resetFields();
dataFormRef.value.clearValidate();
}
// 完全重置 formData 为初始状态
Object.assign(formData, initialFormData);
}
// 行复选框选中项变化
async function handleSelectionChange(selection: any) {
selectIds.value = selection.map((item: any) => item.id);
selectionRows.value = selection;
}
// 关闭弹窗
async function handleCloseDialog() {
dialogVisible.visible = false;
resetForm();
}
// 打开弹窗
async function handleOpenDialog(type: "create" | "update" | "detail", id?: number) {
resetForm();
dialogVisible.type = type;
if (id) {
const response = await AiChatMessageAPI.detailMessage(id);
console.log("详情数据:", response.data.data);
if (type === "detail") {
dialogVisible.title = "详情";
detailFormData.value = { ...response.data.data };
console.log("detailFormData.value:", detailFormData.value);
} else if (type === "update") {
dialogVisible.title = "修改";
Object.assign(formData, response.data.data);
}
} else {
dialogVisible.title = "新增消息";
formData.id = undefined;
}
dialogVisible.visible = true;
}
// 提交表单(防抖)
async function handleSubmit() {
dataFormRef.value.validate(async (valid: any) => {
if (valid) {
loading.value = true;
const submitData = { ...formData };
const id = formData.id;
if (id) {
try {
await AiChatMessageAPI.updateMessage(id, { id, ...submitData });
dialogVisible.visible = false;
resetForm();
handleCloseDialog();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
} else {
try {
await AiChatMessageAPI.createMessage(submitData);
dialogVisible.visible = false;
resetForm();
handleCloseDialog();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
}
}
});
}
// 删除、批量删除
async function handleDelete(ids: number[]) {
ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
loading.value = true;
await AiChatMessageAPI.deleteMessage(ids);
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
})
.catch(() => {
ElMessageBox.close();
});
}
onMounted(() => {
loadingData();
});
</script>
<style lang="scss" scoped>
.empty-content {
font-size: 14px;
color: var(--el-text-color-secondary);
}
</style>
@@ -6,7 +6,7 @@
<template #header>
<div class="card-header">
<span>
对话会话列表
会话记忆
<el-tooltip content="对话会话列表">
<QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip>
@@ -83,7 +83,7 @@
<el-row :gutter="10">
<el-col :span="1.5">
<el-button
v-hasPerm="['module_ai:chat_session:create']"
v-hasPerm="['module_ai:chat:create']"
type="success"
icon="plus"
@click="handleOpenDialog('create')"
@@ -93,7 +93,7 @@
</el-col>
<el-col :span="1.5">
<el-button
v-hasPerm="['module_ai:chat_session:delete']"
v-hasPerm="['module_ai:chat:delete']"
type="danger"
icon="delete"
:disabled="selectIds.length === 0"
@@ -120,7 +120,7 @@
<el-col :span="1.5">
<el-tooltip content="刷新">
<el-button
v-hasPerm="['module_ai:chat_session:query']"
v-hasPerm="['module_ai:chat:query']"
type="primary"
icon="refresh"
circle
@@ -164,11 +164,72 @@
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'session_id')?.show"
label="会话ID"
prop="session_id"
min-width="180"
show-overflow-tooltip
/>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'title')?.show"
label="标题"
prop="title"
min-width="200"
>
<template #default="scope">
<el-input
v-if="editingRowId === scope.row.id"
ref="titleInputRef"
v-model="editingTitle"
size="small"
@blur="handleSaveTitle(scope.row)"
@keyup.enter="handleSaveTitle(scope.row)"
/>
<span v-else class="editable-cell" title="点击编辑" @click="handleEditTitle(scope.row)">
{{ scope.row.title || "未命名会话" }}
<el-icon class="edit-icon"><Edit /></el-icon>
</span>
</template>
</el-table-column>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'user_id')?.show"
label="用户ID"
prop="user_id"
min-width="120"
/>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'team_id')?.show"
label="团队ID"
prop="team_id"
min-width="120"
/>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'team_name')?.show"
label="部门名称"
prop="team_name"
min-width="120"
/>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'agent_id')?.show"
label="Agent ID"
prop="agent_id"
min-width="120"
show-overflow-tooltip
/>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'summary')?.show"
label="会话摘要"
prop="summary"
min-width="200"
show-overflow-tooltip
/>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'message_count')?.show"
label="消息数量"
prop="message_count"
min-width="100"
align="center"
/>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'created_time')?.show"
@@ -182,36 +243,16 @@
prop="updated_time"
min-width="180"
/>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'created_id')?.show"
label="创建人"
prop="created_id"
min-width="120"
>
<template #default="scope">
<el-tag>{{ scope.row.created_by?.name }}</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'updated_id')?.show"
label="更新人"
prop="updated_id"
min-width="120"
>
<template #default="scope">
<el-tag>{{ scope.row.updated_by?.name }}</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'operation')?.show"
fixed="right"
label="操作"
align="center"
min-width="180"
min-width="120"
>
<template #default="scope">
<el-button
v-hasPerm="['module_example:demo:detail']"
v-hasPerm="['module_ai:chat:detail']"
type="info"
size="small"
link
@@ -221,17 +262,7 @@
详情
</el-button>
<el-button
v-hasPerm="['module_example:demo:update']"
type="primary"
size="small"
link
icon="edit"
@click="handleOpenDialog('update', scope.row.id)"
>
编辑
</el-button>
<el-button
v-hasPerm="['module_example:demo:delete']"
v-hasPerm="['module_ai:chat:delete']"
type="danger"
size="small"
link
@@ -259,29 +290,75 @@
<el-dialog
v-model="dialogVisible.visible"
:title="dialogVisible.title"
class="session-detail-dialog"
@close="handleCloseDialog"
>
<!-- 详情 -->
<template v-if="dialogVisible.type === 'detail'">
<el-descriptions :column="4" border>
<el-descriptions :column="2" border>
<el-descriptions-item label="会话ID" :span="2">
{{ detailFormData.session_id }}
</el-descriptions-item>
<el-descriptions-item label="标题" :span="2">
{{ detailFormData.title }}
</el-descriptions-item>
<el-descriptions-item label="创建人" :span="2">
{{ detailFormData.created_by?.name }}
<el-descriptions-item label="用户ID" :span="1">
{{ detailFormData.user_id }}
</el-descriptions-item>
<el-descriptions-item label="更新人" :span="2">
{{ detailFormData.updated_by?.name }}
<el-descriptions-item label="团队ID" :span="1">
{{ detailFormData.team_id }}
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">
<el-descriptions-item label="部门名称" :span="1">
{{ detailFormData.team_name || "未知部门" }}
</el-descriptions-item>
<el-descriptions-item label="Agent ID" :span="1">
{{ detailFormData.agent_id }}
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="1">
{{ detailFormData.created_time }}
</el-descriptions-item>
<el-descriptions-item label="更新时间" :span="2">
<el-descriptions-item label="更新时间" :span="1">
{{ detailFormData.updated_time }}
</el-descriptions-item>
<el-descriptions-item label="消息数量" :span="1">
{{ detailFormData.message_count }}
</el-descriptions-item>
<el-descriptions-item label="会话摘要" :span="2">
{{ detailFormData.summary || "无" }}
</el-descriptions-item>
<el-descriptions-item label="元数据" :span="2">
<pre v-if="detailFormData.metadata">{{
JSON.stringify(detailFormData.metadata, null, 2)
}}</pre>
<span v-else>无</span>
</el-descriptions-item>
</el-descriptions>
<!-- 消息列表 -->
<el-divider content-position="left">消息记录</el-divider>
<el-timeline v-if="detailFormData.messages && detailFormData.messages.length > 0">
<el-timeline-item
v-for="(msg, index) in detailFormData.messages"
:key="index"
:type="msg.role === 'user' ? 'primary' : 'success'"
:icon="msg.role === 'user' ? 'User' : 'ChatDotRound'"
>
<div class="message-item">
<div class="message-header">
<el-tag size="small" :type="msg.role === 'user' ? 'primary' : 'success'">
{{ msg.role === "user" ? "用户" : "助手" }}
</el-tag>
<span v-if="msg.created_at" class="message-time">
{{ formatTime(msg.created_at) }}
</span>
</div>
<div class="message-content">{{ msg.content }}</div>
</div>
</el-timeline-item>
</el-timeline>
<el-empty v-else description="暂无消息记录" :image-size="60" />
</template>
<!-- 新增、编辑表单 -->
<!-- 新增表单 -->
<template v-else>
<el-form
ref="dataFormRef"
@@ -318,44 +395,72 @@ defineOptions({
inheritAttrs: false,
});
import { ref, reactive, onMounted } from "vue";
import { ElMessageBox } from "element-plus";
import AiChatSessionAPI, {
SessionTable,
SessionForm,
SessionPageQuery,
} from "@/api/module_ai/chat_session";
import { ref, reactive, onMounted, nextTick } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import AiChatAPI, { ChatSession } from "@/api/module_ai/chat";
import DatePicker from "@/components/DatePicker/index.vue";
import { QuestionFilled, ArrowUp, ArrowDown } from "@element-plus/icons-vue";
import { QuestionFilled, ArrowUp, ArrowDown, Edit } from "@element-plus/icons-vue";
import { formatToDateTime } from "@/utils/dateUtil";
const visible = ref(true);
const queryFormRef = ref();
const dataFormRef = ref();
const titleInputRef = ref();
const total = ref(0);
const selectIds = ref<number[]>([]);
const selectionRows = ref<SessionTable[]>([]);
const selectIds = ref<string[]>([]);
const selectionRows = ref<ChatSession[]>([]);
const loading = ref(false);
const isExpand = ref(false);
const isExpandable = ref(true);
// 编辑标题相关
const editingRowId = ref<string | null>(null);
const editingTitle = ref("");
// 分页表单
const pageTableData = ref<SessionTable[]>([]);
const pageTableData = ref<ChatSession[]>([]);
// 表格列配置
const tableColumns = ref([
{ prop: "selection", label: "选择框", show: true },
{ prop: "index", label: "序号", show: true },
{ prop: "session_id", label: "会话ID", show: true },
{ prop: "title", label: "标题", show: true },
{ prop: "user_id", label: "用户ID", show: false },
{ prop: "team_id", label: "团队ID", show: false },
{ prop: "team_name", label: "部门名称", show: true },
{ prop: "agent_id", label: "Agent ID", show: false },
{ prop: "summary", label: "会话摘要", show: false },
{ prop: "message_count", label: "消息数量", show: true },
{ prop: "created_time", label: "创建时间", show: true },
{ prop: "updated_time", label: "更新时间", show: true },
{ prop: "created_id", label: "创建人", show: true },
{ prop: "updated_id", label: "更新人", show: true },
{ prop: "operation", label: "操作", show: true },
]);
// 详情表单
const detailFormData = ref<SessionTable>({ title: "" });
const detailFormData = ref<ChatSession>({
session_id: "",
agent_id: null,
team_id: null,
team_name: null,
workflow_id: null,
user_id: null,
session_data: null,
agent_data: null,
team_data: null,
workflow_data: null,
metadata: null,
runs: null,
summary: null,
created_at: null,
updated_at: null,
id: "",
title: null,
created_time: null,
updated_time: null,
message_count: 0,
messages: [],
});
// 日期范围临时变量
const createdDateRange = ref<[Date, Date] | []>([]);
// 更新时间范围临时变量
@@ -365,9 +470,9 @@ const updatedDateRange = ref<[Date, Date] | []>([]);
function handleCreatedDateRangeChange(range: [Date, Date]) {
createdDateRange.value = range;
if (range && range.length === 2) {
queryFormData.created_time = [formatToDateTime(range[0]), formatToDateTime(range[1])];
queryFormData.created_at = [formatToDateTime(range[0]), formatToDateTime(range[1])];
} else {
queryFormData.created_time = undefined;
queryFormData.created_at = undefined;
}
}
@@ -375,25 +480,24 @@ function handleCreatedDateRangeChange(range: [Date, Date]) {
function handleUpdatedDateRangeChange(range: [Date, Date]) {
updatedDateRange.value = range;
if (range && range.length === 2) {
queryFormData.updated_time = [formatToDateTime(range[0]), formatToDateTime(range[1])];
queryFormData.updated_at = [formatToDateTime(range[0]), formatToDateTime(range[1])];
} else {
queryFormData.updated_time = undefined;
queryFormData.updated_at = undefined;
}
}
// 分页查询参数
const queryFormData = reactive<SessionPageQuery>({
const queryFormData = reactive({
page_no: 1,
page_size: 10,
title: undefined,
status: undefined,
created_time: undefined,
updated_time: undefined,
created_at: undefined as string[] | undefined,
updated_at: undefined as string[] | undefined,
});
// 编辑表单
const formData = reactive<SessionForm>({
id: undefined,
const formData = reactive({
id: undefined as string | undefined,
title: "",
});
@@ -401,7 +505,7 @@ const formData = reactive<SessionForm>({
const dialogVisible = reactive({
title: "",
visible: false,
type: "create" as "create" | "update" | "detail",
type: "create" as "create" | "detail",
});
// 表单验证规则
@@ -409,6 +513,12 @@ const rules = reactive({
title: [{ required: true, message: "请输入标题", trigger: "blur" }],
});
// 格式化时间
function formatTime(timestamp: number | null): string {
if (!timestamp) return "";
return formatToDateTime(new Date(timestamp * 1000));
}
// 列表刷新
async function handleRefresh() {
await loadingData();
@@ -418,9 +528,11 @@ async function handleRefresh() {
async function loadingData() {
loading.value = true;
try {
const response = await AiChatSessionAPI.listSession(queryFormData);
pageTableData.value = response.data.data.items;
total.value = response.data.data.total;
const response = await AiChatAPI.getSessionList(queryFormData);
const items = response.data.data?.items;
const validItems = Array.isArray(items) ? items : [];
pageTableData.value = validItems;
total.value = response.data.data?.total || 0;
} catch (error: any) {
console.error(error);
} finally {
@@ -441,14 +553,14 @@ async function handleResetQuery() {
// 重置日期范围选择器
createdDateRange.value = [];
updatedDateRange.value = [];
queryFormData.created_time = undefined;
queryFormData.updated_time = undefined;
queryFormData.created_at = undefined;
queryFormData.updated_at = undefined;
loadingData();
}
// 定义初始表单数据常量
const initialFormData: SessionForm = {
id: undefined,
const initialFormData = {
id: undefined as string | undefined,
title: "",
};
@@ -475,17 +587,15 @@ async function handleCloseDialog() {
}
// 打开弹窗
async function handleOpenDialog(type: "create" | "update" | "detail", id?: number) {
async function handleOpenDialog(type: "create" | "detail", id?: string) {
resetForm();
dialogVisible.type = type;
if (id) {
const response = await AiChatSessionAPI.detailSession(id);
const response = await AiChatAPI.getSessionDetail(id);
if (type === "detail") {
dialogVisible.title = "详情";
Object.assign(detailFormData.value, response.data.data);
} else if (type === "update") {
dialogVisible.title = "修改";
Object.assign(formData, response.data.data);
const sessionData = response.data.data || {};
Object.assign(detailFormData.value, sessionData);
}
} else {
dialogVisible.title = "新增会话";
@@ -494,44 +604,67 @@ async function handleOpenDialog(type: "create" | "update" | "detail", id?: numbe
dialogVisible.visible = true;
}
// 处理编辑标题
function handleEditTitle(row: ChatSession) {
editingRowId.value = row.id;
editingTitle.value = row.title || "";
nextTick(() => {
titleInputRef.value?.focus();
});
}
// 处理保存标题
async function handleSaveTitle(row: ChatSession) {
if (editingRowId.value !== row.id) return;
const newTitle = editingTitle.value.trim();
if (!newTitle) {
ElMessage.warning("标题不能为空");
return;
}
if (newTitle === row.title) {
editingRowId.value = null;
return;
}
try {
loading.value = true;
await AiChatAPI.updateSession(row.id, { title: newTitle });
ElMessage.success("更新成功");
row.title = newTitle;
editingRowId.value = null;
} catch (error: any) {
console.error(error);
ElMessage.error("更新失败");
} finally {
loading.value = false;
}
}
// 提交表单(防抖)
async function handleSubmit() {
dataFormRef.value.validate(async (valid: any) => {
if (valid) {
loading.value = true;
const submitData = { ...formData };
const id = formData.id;
if (id) {
try {
await AiChatSessionAPI.updateSession(id, { id, ...submitData });
dialogVisible.visible = false;
resetForm();
handleCloseDialog();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
} else {
try {
await AiChatSessionAPI.createSession(submitData);
dialogVisible.visible = false;
resetForm();
handleCloseDialog();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
try {
await AiChatAPI.createSession({ title: submitData.title });
dialogVisible.visible = false;
resetForm();
handleCloseDialog();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
}
});
}
// 删除、批量删除
async function handleDelete(ids: number[]) {
async function handleDelete(ids: string[]) {
ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
@@ -540,7 +673,7 @@ async function handleDelete(ids: number[]) {
.then(async () => {
try {
loading.value = true;
await AiChatSessionAPI.deleteSession(ids);
await AiChatAPI.deleteSession(ids);
handleResetQuery();
} catch (error: any) {
console.error(error);
@@ -558,4 +691,64 @@ onMounted(() => {
});
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.edit-icon {
font-size: 12px;
opacity: 0;
transition: opacity 0.2s;
}
.editable-cell {
display: flex;
gap: 8px;
align-items: center;
cursor: pointer;
&:hover {
color: var(--el-color-primary);
.edit-icon {
opacity: 1;
}
}
}
.message-item {
.message-header {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.message-time {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.message-content {
padding: 8px 12px;
word-break: break-all;
white-space: pre-wrap;
background: var(--el-fill-color-light);
border-radius: 4px;
}
}
pre {
max-height: 200px;
padding: 8px;
margin: 0;
overflow: auto;
font-size: 12px;
background: var(--el-fill-color-light);
border-radius: 4px;
}
// 弹窗样式 - 滚动条在弹窗内
:deep(.session-detail-dialog .el-dialog__body) {
max-height: 60vh;
padding: 20px;
overflow-y: auto;
}
</style>