Compare commits
7 Commits
feature/ds
...
feature/zo
| Author | SHA1 | Date | |
|---|---|---|---|
| 848e8a6271 | |||
|
|
3fc26d6996 | ||
|
|
ed04eea481 | ||
|
|
ef50aae9d0 | ||
|
|
c85f211c9c | ||
| c7a37e6816 | |||
|
|
3f2a4a506b |
@@ -6,7 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' http://8.138.234.141; connect-src 'self' http://8.138.234.141 https://api.iconify.design wss://onefeel.brother7.cn"
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' http://8.138.234.141 https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com; connect-src 'self' http://8.138.234.141 https://api.iconify.design wss://onefeel.brother7.cn"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
122
src/renderer/api/SessionsApi.ts
Normal file
122
src/renderer/api/SessionsApi.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { getRequest, postRequest, patchRequest, deleteRequest, ResponseModel } from '@utils/request'
|
||||||
|
|
||||||
|
// 创建会话 的请求参数和响应数据结构
|
||||||
|
export interface CreateSessionRequest {
|
||||||
|
title?: string
|
||||||
|
tenant_id_query?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateSessionResponse {
|
||||||
|
session_id: string
|
||||||
|
user_id: string
|
||||||
|
tenant_id: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSession = async (params: CreateSessionRequest) => {
|
||||||
|
const res: ResponseModel = await postRequest('/nianxx/api/sessions', params)
|
||||||
|
return res.data as CreateSessionResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 获取会话列表 的请求参数和响应数据结构
|
||||||
|
export interface SessionListRequest {
|
||||||
|
tenant_id_query?: string
|
||||||
|
limit?: number
|
||||||
|
offset?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionListResponse {
|
||||||
|
sessions: Array<SessionListRecords>
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionListRecords {
|
||||||
|
session_id: string
|
||||||
|
user_id: string
|
||||||
|
tenant_id: string
|
||||||
|
title: string
|
||||||
|
status: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSessionList = async (params: SessionListRequest) => {
|
||||||
|
const res: ResponseModel = await getRequest('/nianxx/api/sessions', params)
|
||||||
|
return res.data as SessionListResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// 获取会话消息历史 的请求参数和响应数据结构
|
||||||
|
export interface SessionMessagesRequest {
|
||||||
|
user_id_query?: string
|
||||||
|
tenant_id_query?: number
|
||||||
|
limit?: number
|
||||||
|
offset?: number
|
||||||
|
session_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionMessagesResponse {
|
||||||
|
messages: Array<SessionMessageRecords>
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionMessageRecords {
|
||||||
|
id: number
|
||||||
|
session_id: string
|
||||||
|
role: string
|
||||||
|
content: string
|
||||||
|
source: string
|
||||||
|
message_id: string | null
|
||||||
|
created_at: string
|
||||||
|
timestamp?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取会话消息历史 的函数实现
|
||||||
|
export const getSessionMessages = async (params: SessionMessagesRequest) => {
|
||||||
|
const res: ResponseModel = await getRequest(`/nianxx/api/sessions/${params.session_id}/messages`, {
|
||||||
|
limit: params.limit,
|
||||||
|
offset: params.offset,
|
||||||
|
user_id_query: params.user_id_query,
|
||||||
|
tenant_id_query: params.tenant_id_query,
|
||||||
|
})
|
||||||
|
return res.data as SessionMessagesResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// /api/sessions/{session_id} 的请求参数和响应数据结构
|
||||||
|
export interface UpdateSessionRequest {
|
||||||
|
session_id: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateSessionResponse {
|
||||||
|
success: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新会话信息 的函数实现
|
||||||
|
export const updateSession = async (params: UpdateSessionRequest) => {
|
||||||
|
const res: ResponseModel = await postRequest(`/nianxx/api/sessions/${params.session_id}/rename`, {
|
||||||
|
title: params.title,
|
||||||
|
})
|
||||||
|
return res.data as UpdateSessionResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// /api/sessions/{session_id} 的请求参数和响应数据结构
|
||||||
|
export interface DeleteSessionRequest {
|
||||||
|
session_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteSessionResponse {
|
||||||
|
success: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除会话 的函数实现
|
||||||
|
export const deleteSession = async (params: DeleteSessionRequest) => {
|
||||||
|
const res: ResponseModel = await postRequest(`/nianxx/api/sessions/${params.session_id}/delete`, {})
|
||||||
|
return res.data as DeleteSessionResponse
|
||||||
|
}
|
||||||
@@ -77,6 +77,7 @@ instance.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`🚀 ~ request: \n url:${config.url} \n params:${JSON.stringify(config.data)} \n`)
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@@ -87,7 +88,7 @@ instance.interceptors.request.use(
|
|||||||
// 添加响应拦截器
|
// 添加响应拦截器
|
||||||
instance.interceptors.response.use(
|
instance.interceptors.response.use(
|
||||||
(res) => {
|
(res) => {
|
||||||
console.log(`🚀 ~ response: \n url:${res.config.url} \n params:${JSON.stringify(res.config.params)} \n data:\n ${JSON.stringify(res.data)}`)
|
console.log(`🚀 ~ response: \n url:${res.config.url} \n params:${JSON.stringify(res.config.data)} \n data:\n ${JSON.stringify(res.data)}`)
|
||||||
// 未设置状态码则默认成功状态
|
// 未设置状态码则默认成功状态
|
||||||
const code = res.data.code || 200
|
const code = res.data.code || 200
|
||||||
// 获取错误信息
|
// 获取错误信息
|
||||||
@@ -143,7 +144,7 @@ instance.interceptors.response.use(
|
|||||||
export const postRequest = <ResponseModel>(url: string, data?: any, options?: any): Promise<ResponseModel> => {
|
export const postRequest = <ResponseModel>(url: string, data?: any, options?: any): Promise<ResponseModel> => {
|
||||||
return instance.request({
|
return instance.request({
|
||||||
url,
|
url,
|
||||||
method: 'POST',
|
method: 'post',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
@@ -156,12 +157,38 @@ export const postRequest = <ResponseModel>(url: string, data?: any, options?: an
|
|||||||
export const getRequest = <ResponseModel>(url: string, params?: any, options?: any): Promise<ResponseModel> => {
|
export const getRequest = <ResponseModel>(url: string, params?: any, options?: any): Promise<ResponseModel> => {
|
||||||
return instance.request({
|
return instance.request({
|
||||||
url,
|
url,
|
||||||
method: 'GET',
|
method: 'get',
|
||||||
params,
|
params,
|
||||||
...(options || {}),
|
...(options || {}),
|
||||||
}) as Promise<ResponseModel>
|
}) as Promise<ResponseModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 封装基于 request 的 PATCH 请求
|
||||||
|
export const patchRequest = <ResponseModel>(url: string, data?: any, options?: any): Promise<ResponseModel> => {
|
||||||
|
return instance.request({
|
||||||
|
url,
|
||||||
|
method: 'patch',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
...(options || {}),
|
||||||
|
}) as Promise<ResponseModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 封装基于 request 的 DELETE 请求
|
||||||
|
export const deleteRequest = <ResponseModel>(url: string, data?: any, options?: any): Promise<ResponseModel> => {
|
||||||
|
return instance.request({
|
||||||
|
url,
|
||||||
|
method: 'delete',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
...(options || {}),
|
||||||
|
}) as Promise<ResponseModel>
|
||||||
|
}
|
||||||
|
|
||||||
export default instance
|
export default instance
|
||||||
|
|
||||||
/// 响应模型
|
/// 响应模型
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<ChatRoleMe v-if="msg.messageRole === MessageRole.ME" :msg="msg">
|
<ChatRoleMe v-if="msg.messageRole === MessageRole.ME" :msg="msg">
|
||||||
<template #header>
|
<template #header>
|
||||||
<!-- 名字和时间 -->
|
<!-- 名字和时间 -->
|
||||||
<ChatNameTime :showReverse="true" />
|
<ChatNameTime :showReverse="true" :msg="msg" />
|
||||||
</template>
|
</template>
|
||||||
</ChatRoleMe>
|
</ChatRoleMe>
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<ChatRoleAI v-if="msg.messageRole === MessageRole.AI" :msg="msg">
|
<ChatRoleAI v-if="msg.messageRole === MessageRole.AI" :msg="msg">
|
||||||
<template #header>
|
<template #header>
|
||||||
<!-- 名字和时间 -->
|
<!-- 名字和时间 -->
|
||||||
<ChatNameTime :showReverse="false" />
|
<ChatNameTime :showReverse="false" :msg="msg" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -69,7 +69,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, ref, watch, nextTick } from "vue";
|
import { ref, defineProps, defineEmits, watch, nextTick } from 'vue'
|
||||||
|
import { onMounted, onUnmounted } from "vue";
|
||||||
import { WebSocketManager } from "@common/WebSocketManager";
|
import { WebSocketManager } from "@common/WebSocketManager";
|
||||||
import { MessageRole, ChatMessage } from "./model/ChatModel";
|
import { MessageRole, ChatMessage } from "./model/ChatModel";
|
||||||
import { IdUtils } from "@common/index";
|
import { IdUtils } from "@common/index";
|
||||||
@@ -83,11 +84,11 @@ import ChatAttach from './components/ChatAttach.vue';
|
|||||||
import ChatInputArea from './components/ChatInputArea.vue';
|
import ChatInputArea from './components/ChatInputArea.vue';
|
||||||
import TaskCenter from './TaskCenter.vue';
|
import TaskCenter from './TaskCenter.vue';
|
||||||
|
|
||||||
import { Session } from '@utils/storage';
|
import { Session } from '../../utils/storage';
|
||||||
|
|
||||||
import userAvatar from '@assets/images/login/user_icon.png';
|
import userAvatar from '@assets/images/login/user_icon.png';
|
||||||
import aiAvatar from '@assets/images/login/blue_logo.png';
|
import aiAvatar from '@assets/images/login/blue_logo.png';
|
||||||
import { createConversation, conversationMessageList } from '@api/ConversationApi';
|
import { createSession, getSessionMessages } from '../../api/SessionsApi';
|
||||||
import { ElMessage, ElLoading } from 'element-plus'
|
import { ElMessage, ElLoading } from 'element-plus'
|
||||||
|
|
||||||
// 支持外部通过 prop 控制是否为引导页
|
// 支持外部通过 prop 控制是否为引导页
|
||||||
@@ -109,8 +110,8 @@ watch(isGuidePage, (v) => {
|
|||||||
emit('update:guide', v);
|
emit('update:guide', v);
|
||||||
if (v) {
|
if (v) {
|
||||||
// 当切换到引导页时,重置/清理会话状态
|
// 当切换到引导页时,重置/清理会话状态
|
||||||
|
conversationId.value = '';
|
||||||
resetConversation();
|
resetConversation();
|
||||||
createConversationRequest();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,11 +129,15 @@ const isSendingMessage = ref(false);
|
|||||||
const agentId = ref("1953462165250859011");
|
const agentId = ref("1953462165250859011");
|
||||||
/// 会话ID 历史数据接口中获取
|
/// 会话ID 历史数据接口中获取
|
||||||
const conversationId = ref(props.conversationId);
|
const conversationId = ref(props.conversationId);
|
||||||
|
// 标记 conversationId 是否来自历史消息(由 props.conversationId 提供)
|
||||||
|
const conversationIdFromHistory = ref(!!props.conversationId);
|
||||||
|
|
||||||
// 监听 conversationId prop 变化,只有当有值时(选择历史消息)才请求消息列表
|
// 监听 conversationId prop 变化,只有当有值时(选择历史消息)才请求消息列表
|
||||||
watch(() => props.conversationId, (newId) => {
|
watch(() => props.conversationId, (newId) => {
|
||||||
if (newId) {
|
if (newId) {
|
||||||
conversationId.value = newId;
|
conversationId.value = newId;
|
||||||
|
console.log("外部 conversationId 变化,加载对应消息:", newId);
|
||||||
|
conversationIdFromHistory.value = true;
|
||||||
loadConversationMessages(newId);
|
loadConversationMessages(newId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -274,10 +279,9 @@ onMounted(() => {
|
|||||||
|
|
||||||
// token存在,初始化数据
|
// token存在,初始化数据
|
||||||
const initHandler = async () => {
|
const initHandler = async () => {
|
||||||
console.log("initHandler");
|
console.log("initHandler:检查 token 并初始化数据");
|
||||||
const token = getAccessToken();
|
const token = getAccessToken();
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
await createConversationRequest();
|
|
||||||
await initWebSocket();
|
await initWebSocket();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -295,11 +299,13 @@ const checkToken = async () => {
|
|||||||
|
|
||||||
// 调用接口创建新会话
|
// 调用接口创建新会话
|
||||||
const createConversationRequest = async (): Promise<string | null> => {
|
const createConversationRequest = async (): Promise<string | null> => {
|
||||||
const res = await createConversation();
|
const res = await createSession({});
|
||||||
if (res && res.conversationId) {
|
if (res && res.session_id) {
|
||||||
conversationId.value = res.conversationId;
|
conversationId.value = res.session_id;
|
||||||
|
// 新创建的 session 不是来源于历史
|
||||||
|
conversationIdFromHistory.value = false;
|
||||||
console.log("创建新会话,ID:", conversationId.value);
|
console.log("创建新会话,ID:", conversationId.value);
|
||||||
return res.conversationId;
|
return res.session_id;
|
||||||
} else {
|
} else {
|
||||||
console.log("创建会话失败,接口返回异常");
|
console.log("创建会话失败,接口返回异常");
|
||||||
return null;
|
return null;
|
||||||
@@ -309,12 +315,14 @@ const createConversationRequest = async (): Promise<string | null> => {
|
|||||||
// 加载历史会话消息
|
// 加载历史会话消息
|
||||||
const loadConversationMessages = async (convId: string) => {
|
const loadConversationMessages = async (convId: string) => {
|
||||||
try {
|
try {
|
||||||
const res = await conversationMessageList({ conversationId: convId, pageSize: 50, pageNum: 1 });
|
const res = await getSessionMessages({ session_id: convId, limit: 50, offset: 0 });
|
||||||
// 将消息转换为 ChatMessage 格式
|
// 将消息转换为 ChatMessage 格式
|
||||||
chatMsgList.value = res.records.map((msg: any) => ({
|
chatMsgList.value = res.messages.map((msg: any) => ({
|
||||||
messageId: msg.messageId,
|
messageId: msg.message_id,
|
||||||
messageRole: msg.messageSenderRole === 'user' ? MessageRole.ME : MessageRole.AI,
|
messageRole: msg.role === 'user' ? MessageRole.ME : MessageRole.AI,
|
||||||
messageContent: msg.messageContent,
|
messageContent: msg.content,
|
||||||
|
messageContentList: [msg.content],
|
||||||
|
timestamp: msg.created_at_ts,
|
||||||
finished: true, // 历史消息已完成
|
finished: true, // 历史消息已完成
|
||||||
}));
|
}));
|
||||||
console.log("加载历史消息:", chatMsgList.value);
|
console.log("加载历史消息:", chatMsgList.value);
|
||||||
@@ -347,7 +355,8 @@ const initWebSocket = async () => {
|
|||||||
|
|
||||||
// 使用配置的WebSocket服务器地址
|
// 使用配置的WebSocket服务器地址
|
||||||
const token = getAccessToken();
|
const token = getAccessToken();
|
||||||
const wsUrl = `wss://onefeel.brother7.cn/ingress/agent/ws/chat?access_token=${token}`;
|
// const wsUrl = `wss://onefeel.brother7.cn/ingress/agent/ws/chat?access_token=${token}`;
|
||||||
|
const wsUrl = `wss://onefeel.brother7.cn/ingress/nianxx/ws?token=${token}`;
|
||||||
// 初始化WebSocket管理器
|
// 初始化WebSocket管理器
|
||||||
webSocketManager = new WebSocketManager({
|
webSocketManager = new WebSocketManager({
|
||||||
wsUrl: wsUrl,
|
wsUrl: wsUrl,
|
||||||
@@ -407,6 +416,16 @@ const initWebSocket = async () => {
|
|||||||
// 处理WebSocket消息
|
// 处理WebSocket消息
|
||||||
const handleWebSocketMessage = (data: any) => {
|
const handleWebSocketMessage = (data: any) => {
|
||||||
console.log("收到WebSocket消息:", data);
|
console.log("收到WebSocket消息:", data);
|
||||||
|
|
||||||
|
if (data.type === 'notification' && data.event === 'connected') {
|
||||||
|
console.log("WebSocket连接已建立,服务器消息:", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'heartbeat') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 验证关键字段(若服务端传回 conversationId/agentId,则校验是否属于当前会话)
|
// 验证关键字段(若服务端传回 conversationId/agentId,则校验是否属于当前会话)
|
||||||
if (data.conversationId && data.conversationId !== conversationId.value) {
|
if (data.conversationId && data.conversationId !== conversationId.value) {
|
||||||
console.warn("收到不属于当前会话的消息,忽略", data.conversationId);
|
console.warn("收到不属于当前会话的消息,忽略", data.conversationId);
|
||||||
@@ -427,7 +446,7 @@ const handleWebSocketMessage = (data: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 优先使用 messageId 进行匹配
|
// 优先使用 messageId 进行匹配
|
||||||
const msgId = data.messageId || data.id || data.msgId;
|
const msgId = data.messageId || data.reply_message_id || data.id || data.msgId;
|
||||||
let aiMsgIndex = -1;
|
let aiMsgIndex = -1;
|
||||||
if (msgId && pendingMap.has(msgId)) {
|
if (msgId && pendingMap.has(msgId)) {
|
||||||
aiMsgIndex = pendingMap.get(msgId);
|
aiMsgIndex = pendingMap.get(msgId);
|
||||||
@@ -453,16 +472,24 @@ const handleWebSocketMessage = (data: any) => {
|
|||||||
if (chatMsgList.value[aiMsgIndex].isLoading) {
|
if (chatMsgList.value[aiMsgIndex].isLoading) {
|
||||||
// 首次收到内容:替换“加载中”文案并取消 loading 状态(恢复原始渲染逻辑)
|
// 首次收到内容:替换“加载中”文案并取消 loading 状态(恢复原始渲染逻辑)
|
||||||
chatMsgList.value[aiMsgIndex].messageContent = data.content;
|
chatMsgList.value[aiMsgIndex].messageContent = data.content;
|
||||||
|
chatMsgList.value[aiMsgIndex].messageContentList = [data.content];
|
||||||
chatMsgList.value[aiMsgIndex].isLoading = false;
|
chatMsgList.value[aiMsgIndex].isLoading = false;
|
||||||
} else {
|
} else {
|
||||||
// 后续流式内容追加
|
// 后续流式内容追加
|
||||||
chatMsgList.value[aiMsgIndex].messageContent += data.content;
|
chatMsgList.value[aiMsgIndex].messageContent += data.content;
|
||||||
|
chatMsgList.value[aiMsgIndex].messageContentList.push(data.content);
|
||||||
}
|
}
|
||||||
nextTick(() => scrollToBottom());
|
nextTick(() => scrollToBottom());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 对于通知类消息,如果没有明确的完成状态,默认视为已完成,触发后续处理逻辑(例如心跳、连接建立等事件)
|
||||||
|
if (data.type === 'notification') {
|
||||||
|
data.finish = data.finish || true; // 确保 finish 字段存在
|
||||||
|
}
|
||||||
|
|
||||||
// 处理完成状态
|
// 处理完成状态
|
||||||
if (data.finish) {
|
if (data.finish) {
|
||||||
|
chatMsgList.value[aiMsgIndex].timestamp = Date.now();
|
||||||
chatMsgList.value[aiMsgIndex].finished = data.finish;
|
chatMsgList.value[aiMsgIndex].finished = data.finish;
|
||||||
const msg = chatMsgList.value[aiMsgIndex].messageContent;
|
const msg = chatMsgList.value[aiMsgIndex].messageContent;
|
||||||
if (!msg || chatMsgList.value[aiMsgIndex].isLoading) {
|
if (!msg || chatMsgList.value[aiMsgIndex].isLoading) {
|
||||||
@@ -516,6 +543,16 @@ const sendMessage = async (message: string, isInstruct: boolean = false) => {
|
|||||||
|
|
||||||
await checkToken();
|
await checkToken();
|
||||||
|
|
||||||
|
// 如果没有 conversationId(且非历史来源),在发送时按需创建会话
|
||||||
|
if (!conversationId.value) {
|
||||||
|
const sid = await createConversationRequest();
|
||||||
|
if (!sid) {
|
||||||
|
ElMessage({ message: '创建会话失败,请稍后重试', type: 'error' });
|
||||||
|
console.error('createConversationRequest failed before send');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 检查WebSocket连接状态,如果未连接,尝试重新连接
|
// 检查WebSocket连接状态,如果未连接,尝试重新连接
|
||||||
if (!isWsConnected()) {
|
if (!isWsConnected()) {
|
||||||
console.log("WebSocket未连接,尝试重新连接...");
|
console.log("WebSocket未连接,尝试重新连接...");
|
||||||
@@ -553,6 +590,8 @@ const sendMessage = async (message: string, isInstruct: boolean = false) => {
|
|||||||
messageId: IdUtils.generateMessageId(),
|
messageId: IdUtils.generateMessageId(),
|
||||||
messageRole: MessageRole.ME,
|
messageRole: MessageRole.ME,
|
||||||
messageContent: message,
|
messageContent: message,
|
||||||
|
messageContentList: [],
|
||||||
|
timestamp: Date.now(),
|
||||||
};
|
};
|
||||||
chatMsgList.value.push(newMsg);
|
chatMsgList.value.push(newMsg);
|
||||||
inputMessage.value = "";
|
inputMessage.value = "";
|
||||||
@@ -686,6 +725,8 @@ const sendChat = async (message: string, isInstruct = false) => {
|
|||||||
messageId: currentSessionMessageId,
|
messageId: currentSessionMessageId,
|
||||||
messageRole: MessageRole.AI,
|
messageRole: MessageRole.AI,
|
||||||
messageContent: "加载中",
|
messageContent: "加载中",
|
||||||
|
messageContentList: [],
|
||||||
|
timestamp: Date.now(),
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -823,6 +864,10 @@ const resetConversation = () => {
|
|||||||
pendingTimeouts.clear();
|
pendingTimeouts.clear();
|
||||||
pendingMap.clear();
|
pendingMap.clear();
|
||||||
|
|
||||||
|
// 如果 conversationId 不是来自历史,重置 conversationId
|
||||||
|
if (!conversationIdFromHistory.value) {
|
||||||
|
conversationId.value = '';
|
||||||
|
}
|
||||||
// 清理消息与状态
|
// 清理消息与状态
|
||||||
chatMsgList.value = [];
|
chatMsgList.value = [];
|
||||||
inputMessage.value = '';
|
inputMessage.value = '';
|
||||||
|
|||||||
@@ -19,20 +19,62 @@
|
|||||||
]">
|
]">
|
||||||
<span class="w-2 h-2 rounded-full bg-[#BEDBFF] flex-none"></span>
|
<span class="w-2 h-2 rounded-full bg-[#BEDBFF] flex-none"></span>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="truncate text-sm">{{ item.conversationId }}</div>
|
<div class="truncate text-sm">{{ item.conversationTitle }}</div>
|
||||||
</div>
|
</div>
|
||||||
<button v-if="item.conversationId === selectedConversationId"
|
|
||||||
class="bg-transparent border-0 text-gray-500 text-lg px-1 py-0">…</button>
|
<el-dropdown v-if="item.conversationId === selectedConversationId" placement="bottom-end">
|
||||||
|
<el-icon class="el-icon--right">
|
||||||
|
...
|
||||||
|
</el-icon>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item @click="renameHistoryMessage(item.conversationId)">重命名</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="deleteHistoryMessage(item.conversationId)">删除</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<!-- 重命名对话框 -->
|
||||||
|
<el-dialog v-model="renameDialogFormVisible" title="重命名对话" width="500">
|
||||||
|
<el-form :model="newMessageName">
|
||||||
|
<el-form-item label="对话名称" :label-width="formLabelWidth">
|
||||||
|
<el-input v-model="newMessageName" autocomplete="off" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="renameDialogFormVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitNameChange">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 删除确认对话框 -->
|
||||||
|
<el-dialog v-model="deleteDialogVisible" title="温馨提示" width="500">
|
||||||
|
<span>您确定删除该会话吗?删除后将无法恢复!</span>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="deleteDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitDelete">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, defineEmits } from 'vue'
|
import { ref, onMounted, defineEmits } from 'vue'
|
||||||
import { RiAddLine, RiArrowRightSLine, RiArrowDownSLine } from '@remixicon/vue'
|
import { RiAddLine, RiArrowRightSLine, RiArrowDownSLine } from '@remixicon/vue'
|
||||||
import { getConversationList } from '../../api/ConversationApi';
|
import { getSessionList, deleteSession, updateSession } from '../../api/SessionsApi';
|
||||||
|
|
||||||
|
const deleteDialogVisible = ref(false)
|
||||||
|
const renameDialogFormVisible = ref(false)
|
||||||
|
const newMessageName = ref('')
|
||||||
|
const formLabelWidth = '100px'
|
||||||
|
|
||||||
interface HistoryMessage {
|
interface HistoryMessage {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
@@ -51,6 +93,10 @@ const emit = defineEmits(['new-chat', 'select-chat'])
|
|||||||
/// 添加新对话
|
/// 添加新对话
|
||||||
const addNewChat = () => {
|
const addNewChat = () => {
|
||||||
console.log('add new chat')
|
console.log('add new chat')
|
||||||
|
updateNewChat()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateNewChat = () => {
|
||||||
// 触发新对话事件
|
// 触发新对话事件
|
||||||
emit('new-chat')
|
emit('new-chat')
|
||||||
// 清空选择的历史消息ID
|
// 清空选择的历史消息ID
|
||||||
@@ -65,6 +111,43 @@ const selectedHistoryMessage = (conversationId: string) => {
|
|||||||
emit('select-chat', conversationId)
|
emit('select-chat', conversationId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 重命名历史消息
|
||||||
|
const renameHistoryMessage = (conversationId: string) => {
|
||||||
|
console.log('rename message', conversationId)
|
||||||
|
renameDialogFormVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除历史消息
|
||||||
|
const deleteHistoryMessage = (conversationId: string) => {
|
||||||
|
console.log('delete message', conversationId)
|
||||||
|
deleteDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提交重命名
|
||||||
|
const submitNameChange = async () => {
|
||||||
|
console.log('submit name change', newMessageName.value)
|
||||||
|
renameDialogFormVisible.value = false
|
||||||
|
const res = await updateSession({
|
||||||
|
session_id: selectedConversationId.value,
|
||||||
|
title: newMessageName.value
|
||||||
|
})
|
||||||
|
if (res && res.success) {
|
||||||
|
updateNewChat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 提交删除
|
||||||
|
const submitDelete = async () => {
|
||||||
|
console.log('submit delete')
|
||||||
|
deleteDialogVisible.value = false
|
||||||
|
const res = await deleteSession({
|
||||||
|
session_id: selectedConversationId.value
|
||||||
|
})
|
||||||
|
if (res && res.success) {
|
||||||
|
updateNewChat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 页面加载时获取历史会话列表
|
/// 页面加载时获取历史会话列表
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getHistoryConversationList()
|
getHistoryConversationList()
|
||||||
@@ -72,12 +155,13 @@ onMounted(() => {
|
|||||||
|
|
||||||
/// 获取历史会话列表
|
/// 获取历史会话列表
|
||||||
const getHistoryConversationList = async () => {
|
const getHistoryConversationList = async () => {
|
||||||
const list = await getConversationList({ pageSize: 20, pageNum: 1 })
|
const list = await getSessionList({ limit: 50, offset: 0 })
|
||||||
if (!list || !list.records) return;
|
if (!list || !list.sessions) return;
|
||||||
groups.value.push(...list.records.map((item: any) => ({
|
// 使用整体赋值替换 push,避免重复累加
|
||||||
conversationId: item.conversationId,
|
groups.value = list.sessions.map((item: any) => ({
|
||||||
conversationTitle: item.conversationTitle
|
conversationId: item.session_id,
|
||||||
})))
|
conversationTitle: item.title
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,17 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-start gap-2 pt-0.5 mb-2" :class="showReverse ? 'flex-row-reverse' : 'flex-row'">
|
<div class="flex items-start gap-2 pt-0.5 mb-2" :class="props.showReverse ? 'flex-row-reverse' : 'flex-row'">
|
||||||
<span class="text-xs text-[#4E5969]"> ZHINIAN</span>
|
<span class="text-xs text-[#4E5969]">{{ props.msg?.messageRole === MessageRole.AI ? 'NIANXX' : '我' }}</span>
|
||||||
<span class="text-xs text-[#86909C]"> 20:30</span>
|
<span class="text-xs text-[#86909C]">{{ formattedTime }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { ChatMessage, MessageRole } from '../model/ChatModel'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
showReverse: boolean
|
msg?: ChatMessage
|
||||||
|
showReverse?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
showReverse: false
|
showReverse: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const formattedTime = computed(() => {
|
||||||
|
const tsRaw = props.msg?.timestamp
|
||||||
|
if (tsRaw == null) return ''
|
||||||
|
let ts = Number(tsRaw)
|
||||||
|
if (isNaN(ts)) return ''
|
||||||
|
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0')
|
||||||
|
|
||||||
|
// Heuristic:
|
||||||
|
// - If ts < 1e9, treat as a duration in seconds and convert to dd-hh-mm (legacy)
|
||||||
|
// - If ts looks like an epoch (seconds or ms) format to YYYY年MM月DD日 HH:mm:ss
|
||||||
|
if (ts < 1e9) {
|
||||||
|
const totalSeconds = Math.floor(ts)
|
||||||
|
const days = Math.floor(totalSeconds / 86400)
|
||||||
|
const hours = Math.floor((totalSeconds % 86400) / 3600)
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||||
|
return `${String(days).padStart(2, '0')}-${pad(hours)}-${pad(minutes)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// epoch handling: convert seconds -> ms when appropriate
|
||||||
|
if (ts < 1e12) ts = ts * 1000
|
||||||
|
const d = new Date(ts)
|
||||||
|
if (isNaN(d.getTime())) return ''
|
||||||
|
const Y = d.getFullYear()
|
||||||
|
const M = pad(d.getMonth() + 1)
|
||||||
|
const D = pad(d.getDate())
|
||||||
|
const h = pad(d.getHours())
|
||||||
|
const m = pad(d.getMinutes())
|
||||||
|
const s = pad(d.getSeconds())
|
||||||
|
|
||||||
|
// If the timestamp is the same calendar day as today, show only time HH:mm:ss
|
||||||
|
const now = new Date()
|
||||||
|
const sameDay = now.getFullYear() === d.getFullYear()
|
||||||
|
&& now.getMonth() === d.getMonth()
|
||||||
|
&& now.getDate() === d.getDate()
|
||||||
|
|
||||||
|
if (sameDay) {
|
||||||
|
return `${h}:${m}:${s}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise show YYYY-MM-DD HH:mm:ss
|
||||||
|
return `${Y}-${M}-${D} ${h}:${m}:${s}`
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="max-w-[75%] flex flex-col">
|
<div class="max-w-[75%] flex flex-col">
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
<div class="text-sm text-gray-700 flex flex-row">
|
<div v-if="!msg.messageContentList || msg.messageContentList.length === 0"
|
||||||
|
class="flex flex-row text-sm text-gray-700">
|
||||||
<div v-html="compiledMarkdown"></div>
|
<div v-html="compiledMarkdown"></div>
|
||||||
<ChatLoading v-if="msg.isLoading" />
|
<ChatLoading v-if="msg.isLoading" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex flex-col p-2 mb-2 text-sm text-gray-700 bg-[#f7f9fc] rounded-md"
|
||||||
|
v-for="(_, index) in msg.messageContentList" :key="index">
|
||||||
|
<div v-html="compiledAt(index)"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<slot name="footer"></slot>
|
<slot name="footer"></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -23,6 +30,9 @@ interface Props {
|
|||||||
|
|
||||||
const { msg } = defineProps<Props>()
|
const { msg } = defineProps<Props>()
|
||||||
const md = new MarkdownIt({
|
const md = new MarkdownIt({
|
||||||
|
html: true,
|
||||||
|
linkify: true,
|
||||||
|
typographer: true,
|
||||||
highlight: function (str: string, lang: string) {
|
highlight: function (str: string, lang: string) {
|
||||||
if (lang && hljs.getLanguage(lang)) {
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
try {
|
try {
|
||||||
@@ -33,6 +43,18 @@ const md = new MarkdownIt({
|
|||||||
return hljs.highlightAuto(str).value;
|
return hljs.highlightAuto(str).value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const compiledMarkdown = computed(() => md.render(msg.messageContent))
|
const compiledMarkdown = computed(() => md.render(msg.messageContent))
|
||||||
|
|
||||||
|
const compiledList = computed(() => {
|
||||||
|
return (msg.messageContentList || []).map((m: string) => md.render(m || ''))
|
||||||
|
})
|
||||||
|
|
||||||
|
const compiledAt = (index: number): string => {
|
||||||
|
const list: string[] = (compiledList as any).value || []
|
||||||
|
if (list[index]) return list[index]
|
||||||
|
const raw = msg?.messageContentList?.[index] || ''
|
||||||
|
return md.render(raw || '')
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -1,43 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<layout>
|
<layout>
|
||||||
<div class="flex h-full w-full flex-col md:flex-row">
|
<div class="flex h-full w-full flex-col md:flex-row">
|
||||||
<ChatHistory class="flex-none w-50" @new-chat="guide = true" @select-chat="handleSelectChat" />
|
<ChatHistory class="flex-none w-50" @new-chat="handleNewChat" @select-chat="handleSelectChat" />
|
||||||
|
|
||||||
<div class="flex-1 mr-2 overflow-hidden bg-white rounded-xl">
|
<div class="flex-1 mr-2 overflow-hidden bg-white rounded-xl">
|
||||||
<ChatBox v-model:guide="guide" :conversationId="selectedConversationId" />
|
<ChatBox v-model:guide="guide" :conversationId="selectedConversationId" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TaskList />
|
<TaskList />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TaskOperationDialog ref="taskOperationDialogRef" />
|
|
||||||
</layout>
|
</layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
|
||||||
import TaskList from '@renderer/components/TaskList/index.vue'
|
import TaskList from '@renderer/components/TaskList/index.vue'
|
||||||
import TaskOperationDialog from '@renderer/views/home/components/TaskOperationDialog.vue'
|
|
||||||
import ChatHistory from './ChatHistory.vue'
|
import ChatHistory from './ChatHistory.vue'
|
||||||
import ChatBox from './ChatBox.vue'
|
import ChatBox from './ChatBox.vue'
|
||||||
import emitter from '@utils/emitter'
|
import { ref } from 'vue'
|
||||||
|
/// 是否显示引导页
|
||||||
// 是否显示引导页
|
|
||||||
const guide = ref(true)
|
const guide = ref(true)
|
||||||
// 选择的历史会话ID
|
/// 选择的历史会话ID
|
||||||
const selectedConversationId = ref('')
|
const selectedConversationId = ref('')
|
||||||
// 任务操作弹窗引用
|
|
||||||
const taskOperationDialogRef = ref()
|
|
||||||
|
|
||||||
|
/// 处理新对话事件:切换到引导页并清空选中的历史会话ID
|
||||||
|
const handleNewChat = () => {
|
||||||
|
guide.value = true;
|
||||||
|
selectedConversationId.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
// 选择历史会话
|
/// 选择历史会话
|
||||||
const handleSelectChat = (conversationId: string) => {
|
const handleSelectChat = (conversationId: string) => {
|
||||||
guide.value = false;
|
guide.value = false;
|
||||||
selectedConversationId.value = conversationId;
|
selectedConversationId.value = conversationId;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听任务操作弹窗关闭事件
|
|
||||||
emitter.on('OPERATION_CHANNEL', (item) => {
|
|
||||||
taskOperationDialogRef.value?.open(item);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export class ChatMessage {
|
|||||||
messageRole: MessageRole;
|
messageRole: MessageRole;
|
||||||
// 消息内容
|
// 消息内容
|
||||||
messageContent: string;
|
messageContent: string;
|
||||||
|
// 消息内容列表(用于流式更新)
|
||||||
|
messageContentList: string[];
|
||||||
// 是否加载中
|
// 是否加载中
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
// 是否完成
|
// 是否完成
|
||||||
@@ -24,22 +26,28 @@ export class ChatMessage {
|
|||||||
toolCall?: any;
|
toolCall?: any;
|
||||||
// 问题信息
|
// 问题信息
|
||||||
question?: string;
|
question?: string;
|
||||||
|
// 时间戳
|
||||||
|
timestamp?: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
messageId: string,
|
messageId: string,
|
||||||
messageRole: MessageRole,
|
messageRole: MessageRole,
|
||||||
messageContent: string,
|
messageContent: string,
|
||||||
|
messageContentList: string[] = [],
|
||||||
isLoading: boolean = false,
|
isLoading: boolean = false,
|
||||||
finished: boolean = false,
|
finished: boolean = false,
|
||||||
toolCall?: any,
|
toolCall?: any,
|
||||||
question?: any
|
question?: any,
|
||||||
|
timestamp?: number
|
||||||
) {
|
) {
|
||||||
this.messageId = messageId;
|
this.messageId = messageId;
|
||||||
this.messageRole = messageRole;
|
this.messageRole = messageRole;
|
||||||
this.messageContent = messageContent;
|
this.messageContent = messageContent;
|
||||||
|
this.messageContentList = messageContentList;
|
||||||
this.isLoading = isLoading;
|
this.isLoading = isLoading;
|
||||||
this.finished = finished;
|
this.finished = finished;
|
||||||
this.toolCall = toolCall;
|
this.toolCall = toolCall;
|
||||||
this.question = question;
|
this.question = question;
|
||||||
|
this.timestamp = timestamp || Date.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user