1807 lines
60 KiB
Plaintext
1807 lines
60 KiB
Plaintext
import { nextTick } from "vue";
|
||
import {
|
||
MessageInfo,
|
||
ConversationChatParams,
|
||
ConversationChatResponse,
|
||
ConversationChatSuggestParams,
|
||
ProcessingInfo,
|
||
MessageQuestionExtInfo,
|
||
AttachmentFile,
|
||
ConversationCreateParams,
|
||
OpenAppParams,
|
||
ConversationInfo,
|
||
} from "@/types/interfaces/conversationInfo";
|
||
import type {
|
||
AgentSelectedComponentInfo,
|
||
AgentConversationUpdateParams,
|
||
AgentManualComponentInfo,
|
||
} from "@/types/interfaces/agent";
|
||
import { UploadFileInfo } from "@/types/interfaces/common";
|
||
import { RequestResponse } from "@/types/interfaces/request";
|
||
import {
|
||
MessageModeEnum,
|
||
ConversationEventTypeEnum,
|
||
MessageTypeEnum,
|
||
AssistantRoleEnum,
|
||
DefaultSelectedEnum,
|
||
ExpandPageAreaEnum,
|
||
HideChatAreaEnum,
|
||
AgentComponentTypeEnum,
|
||
TaskStatus,
|
||
} from "@/types/enums/agent";
|
||
import { MessageStatusEnum, ProcessingEnum } from "@/types/enums/common";
|
||
import { OpenCloseEnum } from "@/types/enums/space";
|
||
import { SUCCESS_CODE } from "@/constants/codes.constants";
|
||
import { chatService } from "@/utils/chatService";
|
||
import { generateUniqueId } from "@/utils/common";
|
||
import {
|
||
apiAgentConversationCreate,
|
||
apiAgentConversation,
|
||
apiAgentConversationChatSuggest,
|
||
apiAgentConversationUpdate,
|
||
apiAgentConversationChatStop,
|
||
apiTempChatConversationStop,
|
||
apiTempChatConversationQuery,
|
||
apiTempChatConversationCreate,
|
||
apiAgentConversationMessageList,
|
||
} from "@/servers/conversation";
|
||
import {
|
||
apiPublishedAgentInfo,
|
||
apiCollectAgent,
|
||
apiUnCollectAgent,
|
||
apiGetStaticFileList,
|
||
} from "@/servers/agentDev";
|
||
import {
|
||
apiGetSandboxList,
|
||
apiUpdateSelectedSandbox,
|
||
apiRestartAgent,
|
||
apiRestartSandbox,
|
||
} from "@/subpackages/servers/sandbox";
|
||
import { v4 as uuidv4 } from "uuid";
|
||
import AgentDetailData from "./AgentDetailData.uts";
|
||
import { getCustomBlock } from "@/subpackages/utils/containerHelper.uts";
|
||
import { replaceMathBracket } from "@/utils/markdown.uts";
|
||
import { adaptHistoryMessage } from "@/subpackages/utils/historyMessageAdapter";
|
||
import AgentDetailUtils from "./AgentDetailUtils.uts";
|
||
import { TEMP_CONVERSATION_UID } from "@/constants/common.constants";
|
||
import { API_BASE_URL } from "@/constants/config";
|
||
import { CustomActionService } from "@/subpackages/utils/customActionService";
|
||
import { transformFlatListToTree } from "@/subpackages/utils/fileTree";
|
||
import {
|
||
extractTaskResult,
|
||
getFileProxyUrlByConversationIdAndFilePath,
|
||
jumpToFilePreviewPage,
|
||
} from "@/utils/system.uts";
|
||
import { t } from "@/utils/i18n";
|
||
|
||
/** 每页加载消息数量 */
|
||
const MESSAGE_LIST_SIZE = 10;
|
||
|
||
const handleMessageProcess = (text: string): string => {
|
||
return text; // 目前由于方案 已经整体切换到 mp-html内处理 所这里都关闭了
|
||
};
|
||
/**
|
||
* 服务层:负责业务逻辑和API调用
|
||
*/
|
||
export default class AgentDetailService {
|
||
private data: AgentDetailData;
|
||
private isAllowSuggest: boolean;
|
||
constructor(private data: AgentDetailData) {
|
||
this.data = data;
|
||
this.isAllowSuggest = true;
|
||
}
|
||
|
||
/**
|
||
* 处理查询会话
|
||
*/
|
||
async handleQueryConversation(
|
||
result: RequestResponse<ConversationInfo>,
|
||
): Promise<void> {
|
||
this.data.isLoadingConversation.value = true;
|
||
const { data: responseData } = result;
|
||
|
||
this.data.conversationInfo.value =
|
||
responseData as unknown as ConversationInfo;
|
||
this.data.agentInfo.value = responseData?.agent;
|
||
// 确保 agentId 被设置
|
||
if (responseData?.agentId) {
|
||
this.data.agentId.value = responseData.agentId;
|
||
}
|
||
// 确保会话ID被设置,临时会话加载历史记录同样需要该 ID
|
||
if (responseData?.id) {
|
||
this.data.conversationId.value = responseData.id;
|
||
}
|
||
// 是否开启问题建议,可用值:Open,Close
|
||
this.data.isSuggest.value =
|
||
responseData?.agent?.openSuggest === OpenCloseEnum.Open;
|
||
// 可用组件列表
|
||
this.data.manualComponents.value =
|
||
responseData?.agent?.manualComponents || [];
|
||
// 初始化默认选中组件
|
||
this.data.selectedComponents.value = (
|
||
responseData?.agent?.manualComponents || []
|
||
)
|
||
.filter(
|
||
(item: AgentManualComponentInfo): boolean =>
|
||
item.defaultSelected === DefaultSelectedEnum.Yes,
|
||
)
|
||
.map(
|
||
(item: AgentManualComponentInfo): AgentSelectedComponentInfo =>
|
||
({
|
||
id: item.id,
|
||
type: item.type,
|
||
}) as AgentSelectedComponentInfo,
|
||
);
|
||
// 如果变量参数存在,则禁用变量参数
|
||
if (responseData?.variables) {
|
||
this.data.isSendMessageRef.value = true;
|
||
}
|
||
// 变量参数
|
||
const _variables = responseData?.agent?.variables || [];
|
||
this.data.variables.value = _variables;
|
||
// 用户填充变量
|
||
this.data.userFillVariables.value = responseData?.variables || {};
|
||
// 消息列表
|
||
const _messageList = responseData?.messageList || [];
|
||
// 排除预置问题
|
||
const len = _messageList?.filter((item) => !!item.id).length || 0;
|
||
if (len) {
|
||
// 处理消息文本,先适配历史消息格式(PC端HTML标签转移动端Markdown语法)
|
||
const processedMessageList = _messageList.map((message: MessageInfo) => {
|
||
// 适配历史消息:将PC端自定义HTML标签转换为移动端Markdown容器语法
|
||
// 同时将componentExecutedList转换为processingList
|
||
const adaptedMessage = adaptHistoryMessage(message);
|
||
|
||
// 确保 text 字段始终有值,避免 undefined 导致渲染错误
|
||
const rawText = adaptedMessage.text ?? "";
|
||
const text = handleMessageProcess(rawText);
|
||
// 使用同步方法生成ID,兼容所有平台
|
||
const id = adaptedMessage.id || generateUniqueId();
|
||
|
||
return { ...adaptedMessage, text, id };
|
||
});
|
||
|
||
this.data.autoToLastMsg.value = true;
|
||
// 一次性设置消息列表,避免逐条渲染产生动态效果
|
||
this.data.messageList.value = processedMessageList;
|
||
|
||
// 无论初始返回多少条,只要有消息就认为可能有历史消息,保证向上滑动时至少调用一次分页接口
|
||
this.data.hasMoreMessages.value = len > 0;
|
||
|
||
// 处理问题建议
|
||
const lastMessage = processedMessageList[len - 1];
|
||
// 最后一条消息为"问答"时,获取问题建议
|
||
if (
|
||
lastMessage.type === MessageModeEnum.QUESTION &&
|
||
lastMessage.ext?.length
|
||
) {
|
||
const suggestList = lastMessage.ext.map((item) => item.content) || [];
|
||
this.data.chatSuggestList.value = suggestList;
|
||
} else if (len === 1) {
|
||
// 如果消息列表大于1时,说明已开始会话,就不显示预置问题,反之显示
|
||
// 如果存在预置问题,显示预置问题
|
||
this.data.chatSuggestList.value =
|
||
responseData?.agent?.openingGuidQuestions || [];
|
||
}
|
||
}
|
||
// 不存在会话消息时,才显示开场白预置问题
|
||
else {
|
||
this.data.chatSuggestList.value =
|
||
responseData?.agent?.guidQuestionDtos?.slice(0, 5) || [];
|
||
}
|
||
this.data.isLoadingConversation.value = false;
|
||
|
||
// 获取沙盒列表
|
||
this.handleGetSandboxList();
|
||
|
||
// 初始化当前模型ID
|
||
if (responseData?.agent) {
|
||
this.data.currentModelId.value = responseData.modelId || 0;
|
||
}
|
||
|
||
// 检查订阅限制
|
||
if (responseData?.agent != null) {
|
||
const agent = responseData.agent!;
|
||
const paymentRequired = agent.paymentRequired === true;
|
||
const subscribed = agent.subscribed === true;
|
||
const overCallLimit = agent.overCallLimit === true;
|
||
|
||
// 是否弹出模态框:要求订阅且未订阅,或超出调用限制(已订阅时不主动弹窗)
|
||
const shouldShowModal =
|
||
paymentRequired && (!subscribed || overCallLimit) && !subscribed;
|
||
if (shouldShowModal && this.data.enableSubscription.value === 1) {
|
||
this.data.subscriptionModalVisible.value = true;
|
||
}
|
||
|
||
// 是否禁用输入框:要求订阅、且未订阅、且已超出调用限制
|
||
const shouldBlockInput = paymentRequired && !subscribed && overCallLimit;
|
||
if (shouldBlockInput && this.data.enableSubscription.value === 1) {
|
||
this.data.subscriptionRequired.value = true;
|
||
} else {
|
||
this.data.subscriptionRequired.value = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载更多历史消息
|
||
* 用户上滑到顶部时触发,向消息列表头部追加更多历史消息
|
||
*/
|
||
async handleLoadMoreMessages(): Promise<void> {
|
||
const conversationId = this.data.conversationId.value;
|
||
|
||
// 前置校验
|
||
if (
|
||
!conversationId ||
|
||
this.data.isLoadingMoreMessages.value ||
|
||
!this.data.hasMoreMessages.value ||
|
||
this.data.messageList.value.length === 0
|
||
) {
|
||
return;
|
||
}
|
||
|
||
// 取消息列表第一条的 index 作为查询游标
|
||
const firstMessage = this.data.messageList.value[0];
|
||
const currentIndex = firstMessage?.index || 0;
|
||
|
||
// 如果 index 为 0,说明已经是最早的消息
|
||
if (currentIndex <= 0) {
|
||
this.data.hasMoreMessages.value = false;
|
||
return;
|
||
}
|
||
|
||
this.data.isLoadingMoreMessages.value = true;
|
||
|
||
try {
|
||
const res = await apiAgentConversationMessageList({
|
||
conversationId,
|
||
index: currentIndex,
|
||
size: MESSAGE_LIST_SIZE,
|
||
});
|
||
|
||
if (res.code === SUCCESS_CODE && res.data) {
|
||
const olderMessages = res.data;
|
||
|
||
if (olderMessages.length > 0) {
|
||
// 处理消息格式(与 handleQueryConversation 保持一致)
|
||
const processedMessages = olderMessages.map(
|
||
(message: MessageInfo) => {
|
||
const adaptedMessage = adaptHistoryMessage(message);
|
||
const rawText = adaptedMessage.text ?? "";
|
||
const text = handleMessageProcess(rawText);
|
||
const id = adaptedMessage.id || generateUniqueId();
|
||
return { ...adaptedMessage, text, id };
|
||
},
|
||
);
|
||
|
||
// 前插到消息列表头部
|
||
this.data.messageList.value = [
|
||
...processedMessages,
|
||
...this.data.messageList.value,
|
||
];
|
||
|
||
// 判断是否还有更多消息
|
||
this.data.hasMoreMessages.value =
|
||
olderMessages.length >= MESSAGE_LIST_SIZE;
|
||
} else {
|
||
// 返回空数组,没有更多消息
|
||
this.data.hasMoreMessages.value = false;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("[AgentDetailService] 加载更多消息失败:", error);
|
||
} finally {
|
||
this.data.isLoadingMoreMessages.value = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 修改消息列表
|
||
*/
|
||
async handleChangeMessageList(
|
||
res: ConversationChatResponse,
|
||
currentMessageId: string,
|
||
): Promise<void> {
|
||
// 添加空值检查
|
||
if (!res) {
|
||
console.warn("[AgentDetailService] handleChangeMessageList 收到空响应");
|
||
return;
|
||
}
|
||
|
||
// 忽略心跳包 (HEART_BEAT 不包含 data)
|
||
if (res.eventType === "HEART_BEAT") {
|
||
return;
|
||
}
|
||
|
||
// 检查 res.data 是否存在
|
||
if (!res.data) {
|
||
console.warn(
|
||
"[AgentDetailService] handleChangeMessageList 响应中缺少 data 字段:",
|
||
res,
|
||
);
|
||
return;
|
||
}
|
||
|
||
const { data: responseData, eventType } = res;
|
||
this.data.currentConversationRequestId.value = res.requestId;
|
||
|
||
// 调试日志:追踪流式过程
|
||
// console.log(`[Stream] Event: ${eventType}, MessageId: ${currentMessageId}, Text: ${responseData?.text?.substring(0, 50)}...`)
|
||
|
||
if (!this.data.messageList.value?.length) return;
|
||
|
||
const list = [...this.data.messageList.value];
|
||
const index = list.findIndex((item) => item.id === currentMessageId);
|
||
let arraySpliceAction = 1;
|
||
|
||
const currentMessage = list.find(
|
||
(item) => item.id === currentMessageId,
|
||
) as MessageInfo;
|
||
if (!currentMessage) return;
|
||
|
||
// 检查是否是新的对话开始(第一个消息块)
|
||
const isNewConversation =
|
||
!currentMessage.text || currentMessage.text.length === 0;
|
||
|
||
// 确保流式过程中消息始终可见
|
||
if (currentMessage.status === MessageStatusEnum.Loading) {
|
||
currentMessage.status = MessageStatusEnum.Incomplete;
|
||
// console.log(`[Stream] Message ${currentMessageId} status changed from Loading to Incomplete`)
|
||
}
|
||
|
||
let newMessage: MessageInfo | null = null;
|
||
|
||
// 处理不同的事件类型
|
||
if (eventType === ConversationEventTypeEnum.PROCESSING) {
|
||
const processingResult = responseData.result || {};
|
||
responseData.executeId = processingResult.executeId;
|
||
|
||
const accumulatedText = getCustomBlock(
|
||
currentMessage.text || "",
|
||
responseData,
|
||
);
|
||
|
||
newMessage = {
|
||
...currentMessage,
|
||
text: accumulatedText,
|
||
status: MessageStatusEnum.Loading,
|
||
processingList: [
|
||
...(currentMessage?.processingList || []),
|
||
responseData,
|
||
] as ProcessingInfo[],
|
||
};
|
||
|
||
// 长任务型任务处理
|
||
const conversationId = this.data.conversationId.value;
|
||
|
||
// 长任务型任务处理(打开远程桌面)
|
||
// if (
|
||
// responseData.type === AgentComponentTypeEnum.Event &&
|
||
// (responseData as any).subEventType === 'OPEN_DESKTOP' &&
|
||
// conversationId
|
||
// ) {
|
||
// console.log('[AgentDetailService] Action: RECEIVE_EVENT, Params:', { type: 'OPEN_DESKTOP', conversationId, payload: responseData });
|
||
// // 详情见 docs/agent_integration_guide.md
|
||
// CustomActionService.openDesktopView(conversationId);
|
||
// }
|
||
|
||
// 长任务型任务处理(刷新文件树)
|
||
if (
|
||
responseData.type === AgentComponentTypeEnum.ToolCall &&
|
||
conversationId
|
||
) {
|
||
// console.log("[AgentDetailService] Action: RECEIVE_TOOL_CALL, Params:", {
|
||
// type: "REFRESH_FILE_LIST",
|
||
// conversationId,
|
||
// payload: responseData,
|
||
// });
|
||
CustomActionService.refreshFileList(conversationId);
|
||
}
|
||
}
|
||
|
||
if (eventType === ConversationEventTypeEnum.MESSAGE) {
|
||
const { text, type, ext, id, finished } = responseData;
|
||
// 如果会话结束,则设置会话状态为不活跃
|
||
if (finished) {
|
||
this.data.isConversationActive.value = false;
|
||
}
|
||
|
||
if (type === MessageModeEnum.THINK) {
|
||
newMessage = {
|
||
...currentMessage,
|
||
think: `${currentMessage.think}${text}`,
|
||
status: MessageStatusEnum.Incomplete,
|
||
};
|
||
} else if (type === MessageModeEnum.QUESTION) {
|
||
// 问题内容累积 - 新对话时从头开始,否则累积
|
||
const accumulatedText = handleMessageProcess(
|
||
isNewConversation ? text : `${currentMessage.text}${text}`,
|
||
);
|
||
newMessage = {
|
||
...currentMessage,
|
||
text: accumulatedText,
|
||
status: finished
|
||
? MessageStatusEnum.Complete
|
||
: MessageStatusEnum.Incomplete,
|
||
};
|
||
|
||
if (ext?.length) {
|
||
this.data.chatSuggestList.value =
|
||
ext.map((extItem: MessageQuestionExtInfo) => extItem.content) || [];
|
||
}
|
||
} else {
|
||
// 工作流过程输出
|
||
if (
|
||
this.data.messageIdRef.value &&
|
||
this.data.messageIdRef.value !== id &&
|
||
finished
|
||
) {
|
||
// 新对话时从头开始,否则累积
|
||
const accumulatedText = handleMessageProcess(
|
||
isNewConversation ? text : `${currentMessage.text}${text}`,
|
||
);
|
||
newMessage = {
|
||
...currentMessage,
|
||
id,
|
||
text: accumulatedText,
|
||
status: MessageStatusEnum.Complete,
|
||
};
|
||
arraySpliceAction = 0;
|
||
} else {
|
||
this.data.messageIdRef.value = id;
|
||
// 新对话时从头开始,否则累积
|
||
const accumulatedText = handleMessageProcess(
|
||
isNewConversation ? text : `${currentMessage.text}${text}`,
|
||
);
|
||
|
||
newMessage = {
|
||
...currentMessage,
|
||
text: accumulatedText,
|
||
status: MessageStatusEnum.Incomplete,
|
||
};
|
||
}
|
||
}
|
||
}
|
||
|
||
if (eventType === ConversationEventTypeEnum.FINAL_RESULT) {
|
||
newMessage = {
|
||
...currentMessage,
|
||
status: MessageStatusEnum.Complete,
|
||
finalResult: responseData,
|
||
requestId: res.requestId,
|
||
};
|
||
this.data.requestId.value = res.requestId;
|
||
}
|
||
|
||
if (eventType === ConversationEventTypeEnum.ERROR) {
|
||
newMessage = {
|
||
...currentMessage,
|
||
status: MessageStatusEnum.Error,
|
||
};
|
||
this.data.isConversationActive.value = false;
|
||
}
|
||
|
||
// 更新消息列表
|
||
if (newMessage) {
|
||
list.splice(index, arraySpliceAction, newMessage as MessageInfo);
|
||
}
|
||
|
||
// 确保消息列表更新是同步的
|
||
this.data.messageList.value = [...list];
|
||
|
||
// 流式渲染过程中触发自动滚动到底部
|
||
// 确保 autoToLastMsg 为 true,这样 watch 触发后会执行滚动
|
||
if (this.data.autoToLastMsg.value) {
|
||
// 通过事件总线通知主组件执行滚动,兼容微信小程序和 H5
|
||
uni.$emit("streamMessageUpdate");
|
||
}
|
||
|
||
// 调试日志:状态更新
|
||
// if (newMessage) {
|
||
// console.log(`[Stream] Message ${currentMessageId} updated: status=${newMessage.status}, textLength=${newMessage.text?.length || 0}`)
|
||
// }
|
||
|
||
// 使用优化更新方法,只更新需要更新的组件
|
||
if (newMessage && newMessage.messageType === MessageTypeEnum.ASSISTANT) {
|
||
await nextTick();
|
||
// 通过uni事件总线更新 - 现在直接传递文本内容给 mp-html 组件
|
||
uni.$emit("updateMessageComponent", {
|
||
messageId: `${currentMessageId}_${newMessage.index}`,
|
||
data: {
|
||
body: handleMessageProcess(newMessage.text || ""),
|
||
...newMessage,
|
||
},
|
||
});
|
||
|
||
// 打开页面首页
|
||
if (
|
||
responseData.status === ProcessingEnum.EXECUTING &&
|
||
responseData.type === "Page"
|
||
) {
|
||
uni.$emit("page_preview_executing", responseData);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取文件树列表
|
||
async fetchFileList(cId: number): Promise<void> {
|
||
try {
|
||
this.data.isLoadingFiles.value = true;
|
||
const result = await apiGetStaticFileList(cId);
|
||
this.data.isLoadingFiles.value = false;
|
||
if (result.code === SUCCESS_CODE) {
|
||
const { files } = result.data || {};
|
||
|
||
let treeData: FileNode[] = [];
|
||
|
||
// 检查是否是新的扁平格式
|
||
if (Array.isArray(files) && files.length > 0 && files[0].name) {
|
||
treeData = transformFlatListToTree(files);
|
||
} else if (Array.isArray(files)) {
|
||
// 如果是原有的树形格式,直接使用
|
||
treeData = files as FileNode[];
|
||
}
|
||
|
||
this.data.fileList.value = treeData;
|
||
}
|
||
} catch (error) {
|
||
console.error("[AgentDetailService] 获取文件树列表失败:", error);
|
||
} finally {
|
||
this.data.isLoadingFiles.value = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 会话处理
|
||
*/
|
||
async handleConversation(
|
||
params: ConversationChatParams | TempConversationChatParams,
|
||
currentMessageId: string,
|
||
// 是否是临时会话
|
||
isTempChat?: boolean = false,
|
||
// 超时回调
|
||
onTimeout?: () => void,
|
||
): Promise<void> {
|
||
try {
|
||
// 清空问题建议列表
|
||
this.data.chatSuggestList.value = [];
|
||
// 开始会话后,允许获取问题建议
|
||
this.isAllowSuggest = true;
|
||
|
||
// 标记是否已经更新过主题,确保只更新一次
|
||
let hasUpdatedTopic = false;
|
||
|
||
await chatService.sendMessage(
|
||
params,
|
||
// 处理流式数据
|
||
(chunk: string) => {
|
||
try {
|
||
const result = JSON.parse(chunk);
|
||
// 验证解析结果
|
||
if (!result || typeof result !== "object") {
|
||
console.warn(
|
||
"[AgentDetailService] 解析结果无效, type:",
|
||
typeof result,
|
||
"value:",
|
||
result,
|
||
);
|
||
return;
|
||
}
|
||
// 验证是否包含必要字段
|
||
if (!result.hasOwnProperty("data")) {
|
||
console.warn(
|
||
"[AgentDetailService] 解析结果缺少 data 字段, keys:",
|
||
Object.keys(result),
|
||
);
|
||
return;
|
||
}
|
||
|
||
// 流式数据开始时,第一次收到数据时更新主题(只执行一次)
|
||
if (
|
||
!hasUpdatedTopic &&
|
||
!isTempChat &&
|
||
this.data.needUpdateTopicRef.value &&
|
||
this.data.conversationInfo?.value?.topicUpdated !== 1 // 只有未更新过才会执行,避免重复修改标题
|
||
) {
|
||
hasUpdatedTopic = true;
|
||
this.handleUpdateTopic(params.conversationId, params.message);
|
||
}
|
||
|
||
this.handleChangeMessageList(result, currentMessageId);
|
||
} catch (error) {
|
||
console.error(
|
||
"[AgentDetailService] 解析SSE数据失败:",
|
||
error,
|
||
"原始数据长度:",
|
||
chunk?.length,
|
||
"前100字符:",
|
||
chunk?.substring(0, 100),
|
||
);
|
||
}
|
||
},
|
||
// 完成回调
|
||
async () => {
|
||
// 流式消息完成,设置会话状态为不活跃
|
||
this.data.isConversationActive.value = false;
|
||
|
||
// 如果不是临时会话,则处理问题建议,临时会话,不需要处理问题建议
|
||
if (!isTempChat) {
|
||
if (this.data.isSuggest.value && this.isAllowSuggest) {
|
||
const responseData = await apiAgentConversationChatSuggest(
|
||
params as ConversationChatSuggestParams,
|
||
);
|
||
// 更新问题建议列表
|
||
this.data.chatSuggestList.value = responseData?.data ?? [];
|
||
}
|
||
}
|
||
// 会话完成后根据消息列表中的最后一条消息 判断是否存在 <task-result> 标签
|
||
const lastMessage =
|
||
this.data.messageList.value[this.data.messageList.value.length - 1];
|
||
|
||
const taskResult = extractTaskResult(lastMessage.text);
|
||
if (taskResult.hasTaskResult && taskResult.file) {
|
||
try {
|
||
const conversationId = this.data.conversationId.value;
|
||
const fileProxyUrl =
|
||
await getFileProxyUrlByConversationIdAndFilePath(
|
||
conversationId,
|
||
taskResult.file,
|
||
);
|
||
jumpToFilePreviewPage(conversationId, fileProxyUrl);
|
||
} catch (error) {
|
||
console.error(
|
||
"[AgentDetailService] Failed to parse <task-result> from last message:",
|
||
error,
|
||
);
|
||
}
|
||
}
|
||
},
|
||
// 错误回调
|
||
(error: any) => {
|
||
console.error(
|
||
"[AgentDetailService] Chat stream error callback:",
|
||
error,
|
||
);
|
||
onTimeout?.();
|
||
},
|
||
isTempChat,
|
||
// 超时回调
|
||
() => {
|
||
onTimeout?.();
|
||
},
|
||
);
|
||
} catch (error) {
|
||
console.error("[AgentDetailService] Chat handling failed:", error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 停止会话
|
||
* @param isTempChat 是否是临时会话
|
||
*/
|
||
async handleStopConversation(isTempChat: boolean = false): Promise<void> {
|
||
// 停止会话后,不再允许获取问题建议
|
||
this.isAllowSuggest = false;
|
||
|
||
// 如果会话已经不活跃,说明会话已经结束,无需调用 stop 接口
|
||
if (!this.data.isConversationActive.value) {
|
||
this.data.isStoppingConversation.value = false;
|
||
return;
|
||
}
|
||
|
||
// 如果普通会话,并且任务正在执行,则更新任务状态为 CANCEL
|
||
if (this.data.conversationInfo.value?.taskStatus === TaskStatus.EXECUTING) {
|
||
this.data.conversationInfo.value = {
|
||
...this.data.conversationInfo.value,
|
||
taskStatus: TaskStatus.CANCEL,
|
||
};
|
||
}
|
||
|
||
// 会话消息请求ID
|
||
const requestId = this.data.currentConversationRequestId.value;
|
||
|
||
// 对于临时会话,必须要有 requestId 才能停止
|
||
if (isTempChat && !requestId) {
|
||
this.data.isStoppingConversation.value = false;
|
||
this.data.isConversationActive.value = false;
|
||
return;
|
||
}
|
||
|
||
// 对于普通会话,即使没有 requestId 也可以通过 conversationId 停止
|
||
// 显示停止会话loading
|
||
this.data.isStoppingConversation.value = true;
|
||
let res = null;
|
||
try {
|
||
if (isTempChat) {
|
||
res = await apiTempChatConversationStop(requestId);
|
||
} else {
|
||
res = await apiAgentConversationChatStop(
|
||
String(this.data.conversationId.value),
|
||
);
|
||
}
|
||
const { code, data: responseData, message } = res;
|
||
this.data.isStoppingConversation.value = false;
|
||
// 停止会话成功
|
||
if (code === SUCCESS_CODE) {
|
||
this.data.isConversationActive.value = false;
|
||
console.log("[AgentDetailService] stop 接口调用成功");
|
||
} else {
|
||
uni.showToast({
|
||
title: message,
|
||
icon: "none",
|
||
type: "error",
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error("[AgentDetailService] stop 接口调用失败:", error);
|
||
this.data.isStoppingConversation.value = false;
|
||
this.data.isConversationActive.value = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将正在执行的工具调用状态更新为失败
|
||
* 用于超时或异常中断时的状态更新
|
||
*/
|
||
updateProcessingListToFailed(): void {
|
||
console.log("[AgentDetailService] 开始更新工具调用状态为 FAILED");
|
||
|
||
const messageList = this.data.messageList.value;
|
||
if (!messageList || messageList.length === 0) {
|
||
console.log("[AgentDetailService] 消息列表为空,无需更新");
|
||
return;
|
||
}
|
||
|
||
// 查找正在处理中的消息(状态为 Loading 或 Incomplete)
|
||
const updatedMessages = messageList.map((message) => {
|
||
// 只处理正在进行中的助手消息
|
||
if (
|
||
(message.status === MessageStatusEnum.Loading ||
|
||
message.status === MessageStatusEnum.Incomplete) &&
|
||
message.processingList &&
|
||
message.processingList.length > 0
|
||
) {
|
||
console.log(
|
||
`[AgentDetailService] 更新消息 ${message.id} 的工具调用状态`,
|
||
);
|
||
|
||
// 更新 processingList 中的状态
|
||
const updatedProcessingList = message.processingList.map(
|
||
(processing) => {
|
||
// 将所有 EXECUTING 状态的工具调用改为 FAILED
|
||
if (processing.status === ProcessingEnum.EXECUTING) {
|
||
console.log(
|
||
`[AgentDetailService] 工具 ${processing.name} 状态从 EXECUTING 改为 FAILED`,
|
||
);
|
||
return {
|
||
...processing,
|
||
status: ProcessingEnum.FAILED,
|
||
};
|
||
}
|
||
return processing;
|
||
},
|
||
);
|
||
|
||
// 返回更新后的消息
|
||
return {
|
||
...message,
|
||
processingList: updatedProcessingList,
|
||
};
|
||
}
|
||
|
||
return message;
|
||
});
|
||
|
||
// 更新消息列表
|
||
this.data.messageList.value = updatedMessages;
|
||
console.log("[AgentDetailService] 工具调用状态更新完成");
|
||
}
|
||
|
||
/**
|
||
* 获取沙盒列表 (严格模式 - 移动端重构 Phase 9)
|
||
*/
|
||
async handleGetSandboxList(useAgentSelected: boolean = true): Promise<void> {
|
||
if (this.data.isTempChat.value) return;
|
||
try {
|
||
const res = await apiGetSandboxList();
|
||
if (res.code === SUCCESS_CODE && res.data) {
|
||
this.data.sandboxList.value = res.data.sandboxes || [];
|
||
this.data.agentSelectedSandbox.value = useAgentSelected
|
||
? res.data.agentSelected || {}
|
||
: {};
|
||
|
||
const sandboxList = this.data.sandboxList.value;
|
||
const conversationInfo = this.data.conversationInfo.value;
|
||
const agentInfo = this.data.agentInfo.value;
|
||
// 优先使用 data.agentId,其次使用 agentInfo.agentId
|
||
const agentId = String(
|
||
this.data.agentId.value || this.data.agentInfo.value?.agentId || "",
|
||
);
|
||
|
||
// 2.1 权限判断 (UI层已处理置灰,这里主要处理状态)
|
||
if (agentInfo?.hasPermission === false) {
|
||
this.data.isSandboxSwitchDisabled.value = true;
|
||
// this.data.wholeDisabled.value = true;
|
||
// 提示语由UI层 maskText 控制,或者在这里设置 disabledText?
|
||
// 根据需求描述 "置灰对话框,提示:您无该智能体权限"
|
||
// 查看 chat-input-phone 组件,maskText 可能是 prop,但这里好像没有直接控制 maskText 的变量?
|
||
// 假设 UI 层 `chat-input-phone` 的 `has-permission` 属性已经控制了提示。
|
||
// 我们主要确保 switch disabled
|
||
return;
|
||
}
|
||
|
||
// 2.2.1 个人电脑 (Priority 1)
|
||
if (agentInfo && agentInfo.sandboxId) {
|
||
const personalSandboxId = String(agentInfo.sandboxId);
|
||
const exists = sandboxList.some(
|
||
(s) => s.sandboxId === personalSandboxId,
|
||
);
|
||
|
||
if (!exists) {
|
||
// 2.2.1.1 不在列表中 -> 展示“个人电脑不可用”
|
||
this.data.currentSandboxId.value = personalSandboxId; // 或者是空?通常显示名字需要ID匹配,或者UI显示"不可用"
|
||
this.data.isSandboxSwitchDisabled.value = true;
|
||
this.data.isSandboxUnavailable.value = true;
|
||
this.data.sandboxDisabledText.value = t(
|
||
"Mobile.Sandbox.unavailable",
|
||
);
|
||
// this.data.wholeDisabled.value = true;
|
||
} else {
|
||
// 2.2.1.2 在列表中 -> 展示对应的电脑名称且不可切换
|
||
this.data.currentSandboxId.value = personalSandboxId;
|
||
this.data.isSandboxSwitchDisabled.value = true;
|
||
this.data.isSandboxUnavailable.value = false;
|
||
this.data.sandboxDisabledText.value = "";
|
||
// this.data.wholeDisabled.value = false;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 2.2.2 电脑列表为空 (Priority 2)
|
||
if (sandboxList.length === 0) {
|
||
this.data.currentSandboxId.value = "";
|
||
this.data.isSandboxSwitchDisabled.value = true;
|
||
this.data.isSandboxUnavailable.value = true;
|
||
this.data.sandboxDisabledText.value = t(
|
||
"Mobile.Sandbox.noneAvailable",
|
||
);
|
||
// this.data.wholeDisabled.value = true;
|
||
return;
|
||
}
|
||
|
||
// 2.2.3 共享电脑 (Priority 3)
|
||
if (conversationInfo && conversationInfo.sandboxServerId) {
|
||
const sharedSandboxId = String(conversationInfo.sandboxServerId);
|
||
const exists = sandboxList.some(
|
||
(s) => s.sandboxId === sharedSandboxId,
|
||
);
|
||
|
||
if (!exists) {
|
||
// 2.2.3.1 不在列表中 -> 展示“电脑不可用”
|
||
this.data.currentSandboxId.value = sharedSandboxId;
|
||
this.data.isSandboxSwitchDisabled.value = true;
|
||
this.data.isSandboxUnavailable.value = true;
|
||
this.data.sandboxDisabledText.value = t(
|
||
"Mobile.Sandbox.unavailable",
|
||
);
|
||
// this.data.wholeDisabled.value = true;
|
||
} else {
|
||
// 2.2.3.2 在列表中 -> 展示对应的电脑名称且不可切换
|
||
this.data.currentSandboxId.value = sharedSandboxId;
|
||
this.data.isSandboxSwitchDisabled.value = true;
|
||
this.data.isSandboxUnavailable.value = false;
|
||
this.data.sandboxDisabledText.value = "";
|
||
// this.data.wholeDisabled.value = false;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 2.2.4 默认选择 (Priority 4)
|
||
// conversationInfo?.sandboxServerId 和 conversationInfo?.agent?.sandboxId 都为空
|
||
this.data.isSandboxSwitchDisabled.value = false;
|
||
this.data.isSandboxUnavailable.value = false;
|
||
this.data.sandboxDisabledText.value = "";
|
||
// this.data.wholeDisabled.value = false;
|
||
|
||
let initialId = "";
|
||
if (this.data.agentSelectedSandbox.value[agentId]) {
|
||
// agentSelected 优先
|
||
initialId = this.data.agentSelectedSandbox.value[agentId];
|
||
// 校验是否存在,不存在默认第一个
|
||
if (!sandboxList.some((s) => s.sandboxId === initialId)) {
|
||
initialId = sandboxList[0].sandboxId;
|
||
}
|
||
} else {
|
||
// 默认第一个
|
||
initialId = sandboxList[0].sandboxId;
|
||
}
|
||
this.data.currentSandboxId.value = initialId;
|
||
}
|
||
} catch (error) {
|
||
console.error("[AgentDetailService] 获取沙盒列表失败:", error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 切换沙盒
|
||
*/
|
||
async handleSelectSandbox(sandboxId: string): Promise<void> {
|
||
// 如果禁止切换,直接返回
|
||
if (this.data.isSandboxSwitchDisabled.value) {
|
||
console.warn("[AgentDetailService] 当前不允许切换沙盒");
|
||
return;
|
||
}
|
||
|
||
const agentId = String(
|
||
this.data.agentId.value || this.data.agentInfo.value?.agentId || "",
|
||
);
|
||
|
||
try {
|
||
// 调用 API 更新选中记录
|
||
await apiUpdateSelectedSandbox(agentId, sandboxId);
|
||
|
||
// 更新本地状态
|
||
this.data.currentSandboxId.value = sandboxId;
|
||
this.data.agentSelectedSandbox.value[agentId] = sandboxId;
|
||
|
||
// uni.showToast({
|
||
// title: "切换成功",
|
||
// icon: "none",
|
||
// });
|
||
} catch (error) {
|
||
console.error("[AgentDetailService] 切换沙盒失败:", error);
|
||
uni.showToast({
|
||
title: t("Mobile.Sandbox.switchFailed"),
|
||
icon: "none",
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 切换模型
|
||
*/
|
||
handleSelectModel(modelId: number, modelName: string = ""): void {
|
||
this.data.currentModelId.value = modelId;
|
||
if (modelName) {
|
||
this.data.currentModelName.value = modelName;
|
||
}
|
||
this.data.modelSelectVisible.value = false;
|
||
}
|
||
|
||
/**
|
||
* 恢复会话状态
|
||
* 用于页面从后台唤醒后检测并恢复会话
|
||
*
|
||
* 检测异常中断的条件:最后一条 ASSISTANT 消息状态为 Loading/Incomplete 且无 finalResult
|
||
*
|
||
* @returns 是否需要恢复(true 表示检测到异常中断并已恢复)
|
||
*/
|
||
async resumeConversationState(): Promise<boolean> {
|
||
// 1. 检查会话ID
|
||
const conversationId = this.data.conversationId.value;
|
||
if (!conversationId || this.data.isTempChat.value) {
|
||
return false;
|
||
}
|
||
|
||
// 2. 获取最后一条 ASSISTANT 消息
|
||
const messageList = this.data.messageList.value;
|
||
const lastAssistantMessage = [...messageList]
|
||
.reverse()
|
||
.find(
|
||
(msg) => msg.messageType === MessageTypeEnum.ASSISTANT && msg.status,
|
||
);
|
||
|
||
// 3. 获取前端状态
|
||
const isConversationActive = this.data.isConversationActive.value;
|
||
const isStreaming = chatService.isCurrentlyStreaming;
|
||
const isStoppingConversation = this.data.isStoppingConversation.value;
|
||
|
||
// 4. 异常中断检测
|
||
// 条件:最后一条助手消息状态为 Loading/Incomplete 且无 finalResult
|
||
const isAbnormalInterruption =
|
||
lastAssistantMessage &&
|
||
(lastAssistantMessage.status === MessageStatusEnum.Loading ||
|
||
lastAssistantMessage.status === MessageStatusEnum.Incomplete) &&
|
||
!lastAssistantMessage.finalResult;
|
||
|
||
// 5. 如果用户正在主动停止,不是异常中断
|
||
if (isStoppingConversation) {
|
||
return false;
|
||
}
|
||
|
||
// 6. 如果 SSE 仍在工作,无需恢复
|
||
if (isStreaming) {
|
||
return false;
|
||
}
|
||
|
||
// 7. 检测到异常中断
|
||
if (!isAbnormalInterruption) {
|
||
return false;
|
||
}
|
||
|
||
console.log("[SessionResume] 检测到异常中断,正在查询后端会话状态", {
|
||
lastMessageStatus: lastAssistantMessage?.status,
|
||
conversationId,
|
||
});
|
||
|
||
try {
|
||
// 8. 查询后端会话状态并刷新
|
||
const res = await apiAgentConversation(conversationId);
|
||
if (res.code === SUCCESS_CODE) {
|
||
const taskStatus = res.data?.taskStatus;
|
||
// 更新会话状态
|
||
if (taskStatus === "EXECUTING") {
|
||
// 任务仍在后端执行,保持活跃状态
|
||
this.data.isConversationActive.value = true;
|
||
} else {
|
||
// 任务已完成或取消,更新前端状态
|
||
this.data.isConversationActive.value = false;
|
||
}
|
||
|
||
// 更新消息列表(刷新页面数据)
|
||
this.handleQueryConversation(res);
|
||
console.log(
|
||
"[SessionResume] 会话状态已恢复,后端任务状态:",
|
||
taskStatus,
|
||
);
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error("[SessionResume] 恢复会话状态失败:", error);
|
||
// 恢复失败时,重置前端状态
|
||
this.data.isConversationActive.value = false;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送消息
|
||
*
|
||
* @param data 消息数据
|
||
* @param isTempChat 是否是临时会话
|
||
*/
|
||
async handleSendMessage(
|
||
data: {
|
||
// 初始 chat 消息
|
||
messageInfo?: string;
|
||
// 用户上传的文件
|
||
files?: UploadFileInfo[];
|
||
// 用户选择的组件
|
||
selectedComponents?: AgentSelectedComponentInfo[];
|
||
// 用户填写的变量参数
|
||
variableParams?: { [key: string]: string | number };
|
||
// 用户选择的技能ID列表
|
||
skillIds?: number[];
|
||
// 用户选择的模型ID
|
||
modelId?: number;
|
||
// 用户选择的沙盒ID
|
||
sandboxId?: string;
|
||
},
|
||
isTempChat?: boolean = false,
|
||
// 超时回调
|
||
onTimeout?: () => void,
|
||
): Promise<void> {
|
||
const { messageInfo, files, selectedComponents, variableParams } = data;
|
||
// 安全检查:确保this.data存在
|
||
if (!this.data) {
|
||
return;
|
||
}
|
||
|
||
// 清空用户自带的url参数,只在第一次发送消息时使用,后续发送消息时不再使用
|
||
this.data.urlOtherParams.value = null;
|
||
|
||
const attachments: AttachmentFile[] =
|
||
files?.map((file) => ({
|
||
fileKey: file.key || "",
|
||
fileUrl: file.url || "",
|
||
fileName: file.name || "",
|
||
mimeType: file.type || "",
|
||
fileSize: file.size || 0,
|
||
})) || [];
|
||
|
||
// 参数验证
|
||
if ((!messageInfo || !messageInfo?.trim()) && attachments.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// 是否是临时会话
|
||
if (isTempChat) {
|
||
// 链接Key 或 会话唯一标识为空
|
||
if (!this.data.chatKey.value) {
|
||
uni.showToast({
|
||
title: t("Mobile.Conversation.chatKeyRequired"),
|
||
icon: "none",
|
||
type: "error",
|
||
});
|
||
return;
|
||
}
|
||
} else {
|
||
if (!this.data.conversationId.value) {
|
||
uni.showToast({
|
||
title: t("Mobile.Conversation.idRequired"),
|
||
icon: "none",
|
||
type: "error",
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
|
||
// #ifdef WEB || H5
|
||
// 如果不是临时会话页面,并且url中不包含conversationId,则修改url
|
||
if (!isTempChat && !location.href.includes("conversationId")) {
|
||
let newUrl = "";
|
||
|
||
// 如果是应用详情页,则跳转至应用详情页
|
||
if (location.href.includes("app-details")) {
|
||
newUrl = `/subpackages/pages/app-details/app-details?id=${this.data.agentId.value}&conversationId=${this.data.conversationId.value}`;
|
||
} else {
|
||
newUrl = `/subpackages/pages/agent-detail/agent-detail?id=${this.data.agentId.value}&conversationId=${this.data.conversationId.value}`;
|
||
}
|
||
|
||
// 修改url参数且不刷新页面
|
||
const url = new URL(location.href);
|
||
url.hash = newUrl; // 修改参数
|
||
history.replaceState(null, "", url.toString()); // 替换当前历史记录
|
||
}
|
||
// #endif
|
||
|
||
// 使用同步方法生成唯一ID(兼容所有平台)
|
||
const chatMessageId = generateUniqueId();
|
||
const currentMessageId = generateUniqueId();
|
||
|
||
// 构造消息
|
||
const chatMessage = {
|
||
role: AssistantRoleEnum.USER,
|
||
type: MessageModeEnum.CHAT,
|
||
text: messageInfo,
|
||
time: new Date().toISOString(),
|
||
attachments,
|
||
id: chatMessageId,
|
||
messageType: MessageTypeEnum.USER,
|
||
};
|
||
|
||
// 当前助手信息
|
||
const currentMessage = {
|
||
role: AssistantRoleEnum.ASSISTANT,
|
||
type: MessageModeEnum.CHAT,
|
||
text: "",
|
||
think: "",
|
||
time: new Date().toISOString(),
|
||
id: currentMessageId,
|
||
messageType: MessageTypeEnum.ASSISTANT,
|
||
status: MessageStatusEnum.Loading,
|
||
} as MessageInfo;
|
||
|
||
// 将Incomplete状态的消息改为Complete状态,并清理旧的消息状态
|
||
const completeMessageList =
|
||
this.data.messageList.value?.map((item: MessageInfo) => {
|
||
if (item.status === MessageStatusEnum.Incomplete) {
|
||
item.status = MessageStatusEnum.Complete;
|
||
}
|
||
return item;
|
||
}) || [];
|
||
|
||
// 清理旧的消息状态,确保新对话开始时状态是干净的
|
||
this.data.messageIdRef.value = "";
|
||
this.data.currentConversationRequestId.value = "";
|
||
|
||
// 更新消息列表
|
||
const newMessageList = [
|
||
...completeMessageList,
|
||
chatMessage,
|
||
currentMessage,
|
||
] as MessageInfo[];
|
||
// AgentDetailUtils.checkConversationActive(newMessageList, this.data)
|
||
this.data.isConversationActive.value = true;
|
||
this.data.messageList.value = newMessageList;
|
||
|
||
// 基础参数
|
||
const baseParams = {
|
||
variableParams,
|
||
message: messageInfo,
|
||
attachments,
|
||
selectedComponents,
|
||
skillIds: data.skillIds,
|
||
};
|
||
|
||
// 发送消息
|
||
const params: ConversationChatParams | TempConversationChatParams =
|
||
isTempChat
|
||
? {
|
||
chatKey: this.data.chatKey.value,
|
||
conversationUid: this.data.conversationUid.value,
|
||
...baseParams,
|
||
}
|
||
: {
|
||
conversationId: this.data.conversationId.value,
|
||
...baseParams,
|
||
debug: false,
|
||
sandboxId: this.data.currentSandboxId.value || data.sandboxId,
|
||
modelId:
|
||
this.data.currentModelId.value || data.modelId || undefined,
|
||
};
|
||
this.handleConversation(params, currentMessageId, isTempChat, onTimeout);
|
||
}
|
||
|
||
// 根据用户消息更新会话主题
|
||
async handleUpdateTopic(
|
||
conversationId: string,
|
||
messageInfo: string,
|
||
): Promise<void> {
|
||
const params: AgentConversationUpdateParams = {
|
||
id: conversationId,
|
||
firstMessage: messageInfo,
|
||
};
|
||
|
||
const {
|
||
code,
|
||
data: responseData,
|
||
message,
|
||
} = await apiAgentConversationUpdate(params);
|
||
if (code === SUCCESS_CODE) {
|
||
this.data.needUpdateTopicRef.value = responseData?.topicUpdated !== 1;
|
||
// 更新会话主题
|
||
this.data.conversationInfo.value = {
|
||
...this.data.conversationInfo.value,
|
||
topic: responseData?.topic,
|
||
topicUpdated: responseData?.topicUpdated,
|
||
};
|
||
} else {
|
||
uni.showToast({
|
||
title: message,
|
||
icon: "none",
|
||
type: "error",
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清空会话数据
|
||
* @param isTempChat 是否是临时会话
|
||
*/
|
||
clearConversationData(isTempChat: boolean = false): void {
|
||
// 创建新会话后,问题建议列表改为空
|
||
this.data.chatSuggestList.value = [];
|
||
// 创建新会话后,是否正在加载会话改为false
|
||
this.data.isLoadingConversation.value = false;
|
||
// 创建新会话后,消息列表改为空
|
||
this.data.messageList.value = [];
|
||
// 创建新会话后,发送按钮禁用状态改为false
|
||
this.data.wholeDisabled.value = false;
|
||
// 创建新会话后,是否发送过消息改为false, 让对话设置框取消禁用状态
|
||
this.data.isSendMessageRef.value = false;
|
||
// 创建新会话后,用户填写变量参数改为空对象
|
||
this.data.userFillVariables.value = {};
|
||
|
||
// 重新根据 manualComponents 初始化默认选中的组件
|
||
this.data.selectedComponents.value = (
|
||
this.data.manualComponents.value || []
|
||
)
|
||
.filter(
|
||
(item: AgentManualComponentInfo): boolean =>
|
||
item.defaultSelected === DefaultSelectedEnum.Yes,
|
||
)
|
||
.map(
|
||
(item: AgentManualComponentInfo): AgentSelectedComponentInfo =>
|
||
({
|
||
id: item.id,
|
||
type: item.type,
|
||
}) as AgentSelectedComponentInfo,
|
||
);
|
||
|
||
if (!isTempChat) {
|
||
// 创建新会话后,需要更新会话主题
|
||
this.data.needUpdateTopicRef.value = true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建新会话
|
||
*/
|
||
async createNewConversation(isAppDetails: boolean = false): Promise<void> {
|
||
if (!this.data.agentId.value) {
|
||
uni.showToast({
|
||
title: t("Mobile.Conversation.agentIdMissing"),
|
||
icon: "none",
|
||
type: "error",
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 清空会话数据
|
||
this.clearConversationData();
|
||
|
||
// 如果普通会话,并且任务正在执行,则更新任务状态为 CANCEL
|
||
if (this.data.conversationInfo.value?.taskStatus === TaskStatus.EXECUTING) {
|
||
this.data.conversationInfo.value = {
|
||
...this.data.conversationInfo.value,
|
||
taskStatus: TaskStatus.CANCEL,
|
||
};
|
||
}
|
||
|
||
// 会话参数
|
||
const conversationParams: ConversationCreateParams = {
|
||
agentId: this.data.agentId.value,
|
||
devMode: false, // 开发模式, 只有智能体编排页才会设置为true
|
||
};
|
||
|
||
// 获取引导问题
|
||
this.getGuidQuestion(this.data.agentId.value);
|
||
|
||
const {
|
||
code,
|
||
data: responseData,
|
||
message,
|
||
} = await apiAgentConversationCreate(conversationParams);
|
||
if (code === SUCCESS_CODE && responseData && responseData.id) {
|
||
this.data.conversationId.value = responseData.id;
|
||
|
||
// 更新会话信息
|
||
this.data.conversationInfo.value = responseData;
|
||
if (responseData.agent) {
|
||
this.data.agentInfo.value = responseData.agent;
|
||
if (responseData.agent.manualComponents) {
|
||
this.data.manualComponents.value =
|
||
responseData.agent.manualComponents;
|
||
}
|
||
}
|
||
|
||
// 重新基于最新的 manualComponents 初始化默认选中的组件
|
||
this.data.selectedComponents.value = (
|
||
this.data.manualComponents.value || []
|
||
)
|
||
.filter(
|
||
(item: AgentManualComponentInfo): boolean =>
|
||
item.defaultSelected === DefaultSelectedEnum.Yes,
|
||
)
|
||
.map(
|
||
(item: AgentManualComponentInfo): AgentSelectedComponentInfo =>
|
||
({
|
||
id: item.id,
|
||
type: item.type,
|
||
}) as AgentSelectedComponentInfo,
|
||
);
|
||
|
||
// 重新获取沙盒列表以更新状态
|
||
this.handleGetSandboxList(responseData.agent?.allowPrivateSandbox === 1);
|
||
|
||
let newUrl = "";
|
||
|
||
// 如果是应用智能体页面,则跳转至应用智能体页面
|
||
if (isAppDetails) {
|
||
newUrl = `/subpackages/pages/app-details/app-details?id=${this.data.agentId.value}&conversationId=${responseData.id}`;
|
||
} else {
|
||
// 新会话页面URL
|
||
newUrl = `/subpackages/pages/agent-detail/agent-detail?id=${this.data.agentId.value}&conversationId=${responseData.id}`;
|
||
}
|
||
|
||
// #ifdef WEB || H5
|
||
// 修改rul参数且不刷新页面
|
||
const url = new URL(location.href);
|
||
url.hash = newUrl; // 修改参数
|
||
history.replaceState(null, "", url.toString()); // 替换当前历史记录
|
||
// #endif
|
||
|
||
// #ifdef MP-WEIXIN
|
||
uni.redirectTo({
|
||
url: newUrl,
|
||
});
|
||
// #endif
|
||
} else {
|
||
uni.showToast({
|
||
title: message,
|
||
icon: "none",
|
||
type: "error",
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取引导问题
|
||
* @param agentId 智能体id
|
||
*/
|
||
async getGuidQuestion(agentId: number): Promise<void> {
|
||
const {
|
||
code,
|
||
data: responseData,
|
||
message,
|
||
} = await apiPublishedAgentInfo(agentId);
|
||
if (code === SUCCESS_CODE) {
|
||
this.data.chatSuggestList.value =
|
||
responseData?.guidQuestionDtos?.slice(0, 5) ?? [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取基本信息
|
||
* @param appParams 开放应用参数,来自开放应用URL参数params, 包含消息信息、文件信息、组件列表、变量参数等信息
|
||
*/
|
||
async getPublishedAgentInfo(appParams?: OpenAppParams): Promise<void> {
|
||
// 查询已发布的智能体详情接口, 初始化会话id
|
||
const {
|
||
code,
|
||
data: responseData,
|
||
message,
|
||
} = await apiPublishedAgentInfo(
|
||
this.data.agentId.value,
|
||
true, // 开启获取初始化会话id
|
||
);
|
||
|
||
if (code === SUCCESS_CODE) {
|
||
this.data.agentInfo.value = responseData;
|
||
// 初始化可用组件列表和默认选中组件
|
||
this.data.manualComponents.value = responseData?.manualComponents || [];
|
||
this.data.selectedComponents.value = (
|
||
responseData?.manualComponents || []
|
||
)
|
||
.filter(
|
||
(item: AgentManualComponentInfo): boolean =>
|
||
item.defaultSelected === DefaultSelectedEnum.Yes,
|
||
)
|
||
.map(
|
||
(item: AgentManualComponentInfo): AgentSelectedComponentInfo =>
|
||
({
|
||
id: item.id,
|
||
type: item.type,
|
||
}) as AgentSelectedComponentInfo,
|
||
);
|
||
|
||
// 判断是否开启问题建议
|
||
this.data.isSuggest.value =
|
||
responseData?.openSuggest === OpenCloseEnum.Open;
|
||
// 引导问题建议(兼容旧版)
|
||
this.data.chatSuggestList.value =
|
||
responseData?.guidQuestionDtos?.length > 0
|
||
? responseData?.guidQuestionDtos
|
||
: responseData?.openingGuidQuestions?.slice(0, 5);
|
||
// 首页页面URL
|
||
const httpUrl = `${API_BASE_URL}${responseData?.pageHomeIndex ?? ""}`;
|
||
// #ifdef H5 || WEB
|
||
this.data.pageHomeUrl.value =
|
||
process.env.NODE_ENV === "production"
|
||
? `${window.location.origin}${httpUrl}`
|
||
: httpUrl;
|
||
// #endif
|
||
|
||
// #ifdef MP-WEIXIN
|
||
this.data.pageHomeUrl.value = httpUrl;
|
||
// #endif
|
||
|
||
// 变量参数
|
||
const _variables = responseData?.variables || [];
|
||
this.data.variables.value = _variables;
|
||
// 获取会话ID
|
||
this.data.conversationId.value = responseData.conversationId;
|
||
|
||
// 获取沙盒列表
|
||
this.handleGetSandboxList(responseData?.allowPrivateSandbox === 1);
|
||
|
||
// #ifdef H5 || WEB
|
||
try {
|
||
// 如果存在开放应用参数,则发送消息
|
||
if (
|
||
appParams &&
|
||
typeof appParams === "object" &&
|
||
Object.keys(appParams)?.length > 0
|
||
) {
|
||
// 保存开放应用参数
|
||
this.data.appParams.value = appParams;
|
||
const newParams: OpenAppParams = {
|
||
...appParams,
|
||
};
|
||
// 如果存在会话ID,则删除会话唯一标识
|
||
if (
|
||
Object.prototype.hasOwnProperty.call(newParams, "conversationId")
|
||
) {
|
||
delete newParams.conversationId;
|
||
}
|
||
const { message, variableParams, ...urlOtherParams } = newParams;
|
||
// 如果存在其他参数,则保存到urlOtherParams中
|
||
if (
|
||
urlOtherParams &&
|
||
typeof urlOtherParams === "object" &&
|
||
Object.keys(urlOtherParams)?.length > 0
|
||
) {
|
||
this.data.urlOtherParams.value = urlOtherParams;
|
||
}
|
||
// 如果存在变量参数,则保存变量参数
|
||
if (
|
||
variableParams &&
|
||
typeof variableParams === "object" &&
|
||
Object.keys(variableParams)?.length > 0
|
||
) {
|
||
this.data.userFillVariables.value = variableParams;
|
||
}
|
||
// 如果存在消息或文件,则发送消息
|
||
if (message && message.trim()) {
|
||
// 智能体必填变量参数 name 列表(排除系统变量)
|
||
const requiredNames = _variables
|
||
.filter((item: any) => !item.systemVariable && item.require)
|
||
.map((item: any) => item.name);
|
||
|
||
// 用户自带的url参数中的变量参数对象
|
||
const vp =
|
||
variableParams !== null &&
|
||
typeof variableParams === "object" &&
|
||
!Array.isArray(variableParams)
|
||
? (variableParams as Record<string, unknown>)
|
||
: null;
|
||
|
||
// 判断用户自带的url参数中的变量参数是否存在且不为空
|
||
const variableValuePresent = (val: unknown): boolean => {
|
||
if (val === null || val === undefined) return false;
|
||
if (typeof val === "string") return val.trim() !== "";
|
||
if (typeof val === "number") return !Number.isNaN(val);
|
||
if (typeof val === "boolean") return true;
|
||
return false;
|
||
};
|
||
|
||
// 判断用户自带的url参数中的变量参数是否满足智能体必填变量参数要求
|
||
const allRequiredInParams =
|
||
requiredNames.length === 0 ||
|
||
(vp !== null &&
|
||
requiredNames.every(
|
||
(name: string) =>
|
||
Object.prototype.hasOwnProperty.call(vp, name) &&
|
||
variableValuePresent(vp[name]),
|
||
));
|
||
|
||
/**
|
||
* 如果用户自带的url参数中的变量参数满足智能体必填变量参数要求,则发送消息,否则不发送消息
|
||
*/
|
||
if (allRequiredInParams) {
|
||
// 将消息信息转换为消息信息对象
|
||
newParams.messageInfo = message;
|
||
delete newParams.message; // 删除消息信息
|
||
// 初始化会话时,发送消息
|
||
this.handleSendMessage(newParams);
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error(
|
||
"[AgentDetailService] Failed to get open app params:",
|
||
error,
|
||
);
|
||
}
|
||
// #endif
|
||
|
||
// 检查订阅限制
|
||
if (responseData != null) {
|
||
const paymentRequired = responseData!.paymentRequired === true;
|
||
const subscribed = responseData!.subscribed === true;
|
||
const overCallLimit = responseData!.overCallLimit === true;
|
||
|
||
// 是否弹出模态框:要求订阅且未订阅,或超出调用限制(已订阅时不主动弹窗)
|
||
const shouldShowModal =
|
||
paymentRequired && (!subscribed || overCallLimit) && !subscribed;
|
||
if (shouldShowModal && this.data.enableSubscription.value === 1) {
|
||
this.data.subscriptionModalVisible.value = true;
|
||
} else {
|
||
this.data.subscriptionModalVisible.value = false;
|
||
}
|
||
|
||
// 是否禁用输入框:要求订阅、且未订阅、且已超出调用限制
|
||
const shouldBlockInput =
|
||
paymentRequired && !subscribed && overCallLimit;
|
||
if (shouldBlockInput && this.data.enableSubscription.value === 1) {
|
||
this.data.subscriptionRequired.value = true;
|
||
} else {
|
||
this.data.subscriptionRequired.value = false;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 收藏/取消收藏
|
||
*/
|
||
async handleCollect(): Promise<void> {
|
||
if (!this.data.agentInfo.value) return;
|
||
|
||
if (this.data.agentInfo.value.collect) {
|
||
const { code } = await apiUnCollectAgent(
|
||
this.data.agentInfo.value.agentId,
|
||
);
|
||
if (code === SUCCESS_CODE) {
|
||
uni.showToast({
|
||
title: t("Mobile.Conversation.unfavoriteSuccess"),
|
||
icon: "success",
|
||
duration: 2000,
|
||
});
|
||
this.data.agentInfo.value.collect = false;
|
||
}
|
||
} else {
|
||
const { code } = await apiCollectAgent(this.data.agentInfo.value.agentId);
|
||
if (code === SUCCESS_CODE) {
|
||
uni.showToast({
|
||
title: t("Mobile.Conversation.favoriteSuccess"),
|
||
icon: "success",
|
||
duration: 2000,
|
||
});
|
||
this.data.agentInfo.value.collect = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查询临时会话信息
|
||
* conversationUid: 会话唯一标识 (可选)
|
||
*/
|
||
async queryTempConversation(conversationUid: string) {
|
||
// 链接Key为空
|
||
if (!this.data.chatKey.value) {
|
||
uni.showToast({
|
||
title: t("Mobile.Conversation.chatKeyRequired"),
|
||
icon: "none",
|
||
type: "error",
|
||
});
|
||
return;
|
||
}
|
||
const res = await apiTempChatConversationQuery({
|
||
chatKey: this.data.chatKey.value,
|
||
conversationUid,
|
||
});
|
||
if (res.code === SUCCESS_CODE) {
|
||
// 临时会话页, 设置导航栏标题
|
||
if (res.data?.agent) {
|
||
uni.setNavigationBarTitle({
|
||
title: t("Mobile.Conversation.startWithAgent", {
|
||
name: res.data?.agent?.name || "",
|
||
}),
|
||
});
|
||
}
|
||
|
||
// 处理消息列表
|
||
this.handleQueryConversation(res);
|
||
} else {
|
||
this.data.isLoadingConversation.value = false;
|
||
uni.showToast({
|
||
title: res.message,
|
||
icon: "none",
|
||
duration: 2000,
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 创建临时会话
|
||
* chatKey 链接Key
|
||
* @param captchaVerifyParam 验证码参数 (可选)
|
||
*/
|
||
async createTempChatConversation(captchaVerifyParam: string) {
|
||
// 链接Key为空
|
||
if (!this.data.chatKey.value) {
|
||
uni.showToast({
|
||
title: t("Mobile.Conversation.chatKeyRequired"),
|
||
icon: "none",
|
||
type: "error",
|
||
});
|
||
return;
|
||
}
|
||
const params = {
|
||
chatKey: this.data.chatKey.value,
|
||
captchaVerifyParam,
|
||
};
|
||
const res = await apiTempChatConversationCreate(params);
|
||
const { data: resData, code, message } = res;
|
||
// 创建临时会话成功
|
||
if (code === SUCCESS_CODE) {
|
||
this.data.conversationUid.value = resData.uid;
|
||
uni.setStorageSync(TEMP_CONVERSATION_UID, resData.uid);
|
||
// 查询临时会话详细
|
||
this.queryTempConversation(resData.uid);
|
||
} else {
|
||
this.data.isLoadingConversation.value = false;
|
||
uni.showToast({
|
||
title: message,
|
||
icon: "none",
|
||
duration: 2000,
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重启智能体
|
||
*/
|
||
async handleRestartAgent(): Promise<void> {
|
||
const conversationId = this.data.conversationId.value;
|
||
if (!conversationId) return;
|
||
|
||
uni.showLoading({
|
||
title: t("Mobile.Common.restarting"),
|
||
mask: true,
|
||
});
|
||
try {
|
||
const res = await apiRestartAgent(conversationId);
|
||
uni.hideLoading();
|
||
if (res.code === SUCCESS_CODE) {
|
||
uni.showToast({
|
||
title: t("Mobile.Sandbox.restartAgentSuccess"),
|
||
icon: "success",
|
||
});
|
||
} else {
|
||
uni.showToast({
|
||
title: res.message || t("Mobile.Sandbox.restartAgentFailed"),
|
||
icon: "none",
|
||
});
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
console.error("[AgentDetailService] 重启智能体失败:", error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重启容器/客户端
|
||
* @param isCloudComp 是否是云电脑 (currentSandboxId === '-1')
|
||
*/
|
||
async handleRestartSandbox(isCloudComp: boolean): Promise<void> {
|
||
const conversationId = this.data.conversationId.value;
|
||
if (!conversationId) return;
|
||
|
||
uni.showLoading({
|
||
title: t("Mobile.Common.restarting"),
|
||
mask: true,
|
||
});
|
||
try {
|
||
const res = await apiRestartSandbox(conversationId);
|
||
uni.hideLoading();
|
||
if (res.code === SUCCESS_CODE) {
|
||
uni.showToast({
|
||
title: isCloudComp
|
||
? t("Mobile.Sandbox.restartCloudCompSuccess")
|
||
: t("Mobile.Sandbox.restartClientSuccess"),
|
||
icon: "success",
|
||
});
|
||
} else {
|
||
uni.showToast({
|
||
title: res.message || t("Mobile.Common.operationFailed"),
|
||
icon: "none",
|
||
});
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
console.error("[AgentDetailService] 重启容器失败:", error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 刷新或重新查询会话订阅与限制状态
|
||
*/
|
||
async handleCheckSubscriptionLimit(): Promise<void> {
|
||
const id = this.data.conversationId.value;
|
||
if (id) {
|
||
const res = await apiAgentConversation(id);
|
||
if (res.code === SUCCESS_CODE && res.data != null) {
|
||
const agent = res.data!.agent;
|
||
if (agent != null) {
|
||
this.data.agentInfo.value = agent;
|
||
const paymentRequired = agent.paymentRequired === true;
|
||
const subscribed = agent.subscribed === true;
|
||
const overCallLimit = agent.overCallLimit === true;
|
||
|
||
// 是否弹出模态框:要求订阅且未订阅,或超出调用限制(已订阅时不主动弹窗)
|
||
const shouldShowModal =
|
||
paymentRequired && (!subscribed || overCallLimit) && !subscribed;
|
||
if (shouldShowModal && this.data.enableSubscription.value === 1) {
|
||
this.data.subscriptionModalVisible.value = true;
|
||
} else {
|
||
this.data.subscriptionModalVisible.value = false;
|
||
}
|
||
|
||
// 是否禁用输入框:要求订阅、且未订阅、且已超出调用限制
|
||
const shouldBlockInput =
|
||
paymentRequired && !subscribed && overCallLimit;
|
||
if (shouldBlockInput && this.data.enableSubscription.value === 1) {
|
||
this.data.subscriptionRequired.value = true;
|
||
} else {
|
||
this.data.subscriptionRequired.value = false;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|