-
{fileIcon}
-
-
- {fileName.length > 20
- ? fileName.substring(0, 20) + "..."
- : fileName}
-
-
{
- try {
- window.open(messageData.url, "_blank");
- } catch (e) {
- console.error("文件打开失败:", e);
- }
- }}
- >
- 点击查看
-
-
+ return (
+
+
{
+ if (isUrlAvailable) {
+ window.open(url, "_blank");
+ } else if (!isDownloading) {
+ handleFileDownload();
+ }
+ }}
+ >
+
{fileIcon}
+
+
+ {resolvedFileName.length > 20
+ ? resolvedFileName.substring(0, 20) + "..."
+ : resolvedFileName}
+
+
+ {actionText}
- );
- }
- }
-
- // 验证传统JSON格式的小程序数据结构
- // if (
- // messageData &&
- // typeof messageData === "object" &&
- // (messageData.title || messageData.appName)
- // ) {
- // return (
- //
- //
- // {messageData.thumb && (
- //

{
- // const target = e.target as HTMLImageElement;
- // target.style.display = "none";
- // }}
- // />
- // )}
- //
- //
- // {messageData.title || "小程序消息"}
- //
- // {messageData.appName && (
- //
- // {messageData.appName}
- //
- // )}
- //
- //
- //
- // );
- // }
+
+
+ );
}
return renderErrorMessage("[小程序/文件消息]");
diff --git a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx
index 715c755b..1da7da52 100644
--- a/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx
+++ b/Touchkebao/src/pages/pc/ckbox/weChat/components/ChatWindow/components/MessageRecord/index.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef, useState } from "react";
+import React, { CSSProperties, useEffect, useRef, useState } from "react";
import { Avatar, Checkbox } from "antd";
import { UserOutlined, LoadingOutlined } from "@ant-design/icons";
import AudioMessage from "./components/AudioMessage/AudioMessage";
@@ -17,6 +17,117 @@ import { useCustomerStore } from "@weChatStore/customer";
import { fetchReCallApi, fetchVoiceToTextApi } from "./api";
import TransmitModal from "./components/TransmitModal";
+const IMAGE_EXT_REGEX = /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i;
+const FILE_EXT_REGEX = /\.(pdf|doc|docx|xls|xlsx|ppt|pptx|txt|zip|rar|7z)$/i;
+const DEFAULT_IMAGE_STYLE: CSSProperties = {
+ maxWidth: "200px",
+ maxHeight: "200px",
+ borderRadius: "8px",
+};
+const EMOJI_IMAGE_STYLE: CSSProperties = {
+ maxWidth: "120px",
+ maxHeight: "120px",
+};
+
+type ImageContentOptions = {
+ src: string;
+ alt: string;
+ fallbackText: string;
+ style?: CSSProperties;
+ wrapperClassName?: string;
+ withBubble?: boolean;
+ onClick?: () => void;
+};
+
+const openInNewTab = (url: string) => window.open(url, "_blank");
+
+const handleImageError = (
+ event: React.SyntheticEvent
,
+ fallbackText: string,
+) => {
+ const target = event.target as HTMLImageElement;
+ const parent = target.parentElement;
+ if (parent) {
+ parent.innerHTML = `${fallbackText}
`;
+ }
+};
+
+const renderImageContent = ({
+ src,
+ alt,
+ fallbackText,
+ style = DEFAULT_IMAGE_STYLE,
+ wrapperClassName = styles.imageMessage,
+ withBubble = false,
+ onClick,
+}: ImageContentOptions) => {
+ const imageNode = (
+
+

openInNewTab(src))}
+ onError={event => handleImageError(event, fallbackText)}
+ />
+
+ );
+
+ if (withBubble) {
+ return {imageNode}
;
+ }
+
+ return imageNode;
+};
+
+const renderEmojiContent = (src: string) =>
+ renderImageContent({
+ src,
+ alt: "表情包",
+ fallbackText: "[表情包加载失败]",
+ style: EMOJI_IMAGE_STYLE,
+ wrapperClassName: styles.emojiMessage,
+ });
+
+const renderFileContent = (url: string) => {
+ const fileName = url.split("/").pop()?.split("?")[0] || "文件";
+ const displayName =
+ fileName.length > 20 ? `${fileName.substring(0, 20)}...` : fileName;
+
+ return (
+
+
+
📄
+
+
{displayName}
+
openInNewTab(url)}>
+ 点击查看
+
+
+
+
+ );
+};
+
+const isHttpUrl = (value: string) => /^https?:\/\//i.test(value);
+const isHttpImageUrl = (value: string) =>
+ isHttpUrl(value) && IMAGE_EXT_REGEX.test(value);
+const isFileUrl = (value: string) =>
+ isHttpUrl(value) && FILE_EXT_REGEX.test(value);
+
+const isLegacyEmojiContent = (content: string) =>
+ IMAGE_EXT_REGEX.test(content) ||
+ content.includes("emoji") ||
+ content.includes("sticker");
+
+const tryParseContentJson = (content: string): Record | null => {
+ try {
+ return JSON.parse(content);
+ } catch (error) {
+ return null;
+ }
+};
+
interface MessageRecordProps {
contract: ContractData | weChatGroup;
}
@@ -122,6 +233,115 @@ const MessageRecord: React.FC = ({ contract }) => {
return parts;
};
+ const renderUnknownContent = (
+ rawContent: string,
+ trimmedContent: string,
+ msg?: ChatRecord,
+ contract?: ContractData | weChatGroup,
+ ) => {
+ if (isLegacyEmojiContent(trimmedContent)) {
+ return renderEmojiContent(rawContent);
+ }
+
+ const jsonData = tryParseContentJson(trimmedContent);
+
+ if (jsonData && typeof jsonData === "object") {
+ if (jsonData.type === "file" && msg && contract) {
+ return (
+
+ );
+ }
+
+ if (jsonData.type === "link" && jsonData.title && jsonData.url) {
+ const { title, desc, thumbPath, url } = jsonData;
+
+ return (
+
+
openInNewTab(url)}
+ >
+ {thumbPath && (
+

{
+ const target = event.target as HTMLImageElement;
+ target.style.display = "none";
+ }}
+ />
+ )}
+
+
{title}
+ {desc &&
{desc}
}
+
+
+
链接
+
+ );
+ }
+
+ if (jsonData.previewImage && (jsonData.tencentUrl || jsonData.videoUrl)) {
+ const previewImageUrl = String(jsonData.previewImage).replace(
+ /[`"']/g,
+ "",
+ );
+ return (
+
+
+

{
+ const videoUrl = jsonData.videoUrl || jsonData.tencentUrl;
+ if (videoUrl) {
+ openInNewTab(videoUrl);
+ }
+ }}
+ onError={event => {
+ const target = event.target as HTMLImageElement;
+ const parent = target.parentElement?.parentElement;
+ if (parent) {
+ parent.innerHTML = `
[视频预览加载失败]
`;
+ }
+ }}
+ />
+
+
+
+ );
+ }
+ }
+
+ if (isHttpImageUrl(trimmedContent)) {
+ return renderImageContent({
+ src: rawContent,
+ alt: "图片消息",
+ fallbackText: "[图片加载失败]",
+ });
+ }
+
+ if (isFileUrl(trimmedContent)) {
+ return renderFileContent(trimmedContent);
+ }
+
+ return (
+ {parseEmojiText(rawContent)}
+ );
+ };
+
useEffect(() => {
const prevMessages = prevMessagesRef.current;
@@ -190,290 +410,84 @@ const MessageRecord: React.FC = ({ contract }) => {
{fallbackText}
);
- // 添加调试信息
- // console.log("MessageRecord - msgType:", msgType, "content:", content);
+ const isStringValue = typeof content === "string";
+ const rawContent = isStringValue ? content : "";
+ const trimmedContent = rawContent.trim();
- // 根据msgType进行消息类型判断
switch (msgType) {
case 1: // 文本消息
return (
-
{parseEmojiText(content)}
+
+ {parseEmojiText(rawContent)}
+
);
case 3: // 图片消息
- // 验证是否为有效的图片URL
- if (typeof content !== "string" || !content.trim()) {
+ if (!isStringValue || !trimmedContent) {
return renderErrorMessage("[图片消息 - 无效链接]");
}
- return (
-
-
-

window.open(content, "_blank")}
- onError={e => {
- const target = e.target as HTMLImageElement;
- const parent = target.parentElement;
- if (parent) {
- parent.innerHTML = `
[图片加载失败]
`;
- }
- }}
- />
-
-
- );
+ return renderImageContent({
+ src: rawContent,
+ alt: "图片消息",
+ fallbackText: "[图片加载失败]",
+ withBubble: true,
+ });
case 34: // 语音消息
- if (typeof content !== "string" || !content.trim()) {
+ if (!isStringValue || !trimmedContent) {
return renderErrorMessage("[语音消息 - 无效内容]");
}
- // content直接是音频URL字符串
- return ;
+ return ;
case 43: // 视频消息
return (
-
+
);
case 47: // 动图表情包(gif、其他表情包)
- if (typeof content !== "string" || !content.trim()) {
+ if (!isStringValue || !trimmedContent) {
return renderErrorMessage("[表情包 - 无效链接]");
}
- // 使用工具函数判断表情包URL
- if (isEmojiUrl(content)) {
- return (
-
-

window.open(content, "_blank")}
- onError={e => {
- const target = e.target as HTMLImageElement;
- const parent = target.parentElement;
- if (parent) {
- parent.innerHTML = `
[表情包加载失败]
`;
- }
- }}
- />
-
- );
+ if (isEmojiUrl(trimmedContent)) {
+ return renderEmojiContent(rawContent);
}
return renderErrorMessage("[表情包]");
case 48: // 定位消息
- return ;
+ return ;
case 49: // 小程序/文章/其他:图文、文件
- return ;
+ return (
+
+ );
case 10002: // 系统推荐备注消息
- return ;
+ return (
+
+ );
default: {
- // 兼容旧版本和未知消息类型的处理逻辑
- if (typeof content !== "string" || !content.trim()) {
+ if (!isStringValue || !trimmedContent) {
return renderErrorMessage(
`[未知消息类型${msgType ? ` - ${msgType}` : ""}]`,
);
}
- // 智能识别消息类型(兼容旧版本数据)
- const contentStr = content.trim();
-
- // 1. 检查是否为表情包(兼容旧逻辑)
- const isLegacyEmoji =
- contentStr.includes("ac-weremote-s2.oss-cn-shenzhen.aliyuncs.com") ||
- /\.(gif|webp|png|jpg|jpeg)$/i.test(contentStr) ||
- contentStr.includes("emoji") ||
- contentStr.includes("sticker");
-
- if (isLegacyEmoji) {
- return (
-
-

window.open(contentStr, "_blank")}
- onError={e => {
- const target = e.target as HTMLImageElement;
- const parent = target.parentElement;
- if (parent) {
- parent.innerHTML = `
[表情包加载失败]
`;
- }
- }}
- />
-
- );
- }
-
- // 2. 检查是否为JSON格式消息(包括视频、链接等)
- if (contentStr.startsWith("{") && contentStr.endsWith("}")) {
- try {
- const jsonData = JSON.parse(contentStr);
-
- // 检查是否为链接类型消息
- if (jsonData.type === "link" && jsonData.title && jsonData.url) {
- const { title, desc, thumbPath, url } = jsonData;
-
- return (
-
-
window.open(url, "_blank")}
- >
- {thumbPath && (
-

{
- const target = e.target as HTMLImageElement;
- target.style.display = "none";
- }}
- />
- )}
-
-
{title}
- {desc && (
-
{desc}
- )}
-
-
-
链接
-
- );
- }
-
- // 检查是否为视频消息(兼容旧逻辑)
- if (
- jsonData &&
- typeof jsonData === "object" &&
- jsonData.previewImage &&
- (jsonData.tencentUrl || jsonData.videoUrl)
- ) {
- const previewImageUrl = String(jsonData.previewImage).replace(
- /[`"']/g,
- "",
- );
- return (
-
-
-

{
- const videoUrl =
- jsonData.videoUrl || jsonData.tencentUrl;
- if (videoUrl) {
- window.open(videoUrl, "_blank");
- }
- }}
- onError={e => {
- const target = e.target as HTMLImageElement;
- const parent = target.parentElement?.parentElement;
- if (parent) {
- parent.innerHTML = `
[视频预览加载失败]
`;
- }
- }}
- />
-
-
-
- );
- }
- } catch (e) {
- console.warn("兼容模式JSON解析失败:", e);
- }
- }
-
- // 3. 检查是否为图片链接
- const isImageUrl =
- contentStr.startsWith("http") &&
- /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(contentStr);
-
- if (isImageUrl) {
- return (
-
-

window.open(contentStr, "_blank")}
- onError={e => {
- const target = e.target as HTMLImageElement;
- const parent = target.parentElement;
- if (parent) {
- parent.innerHTML = `
[图片加载失败]
`;
- }
- }}
- />
-
- );
- }
-
- // 4. 检查是否为文件链接
- const isFileLink =
- contentStr.startsWith("http") &&
- /\.(pdf|doc|docx|xls|xlsx|ppt|pptx|txt|zip|rar|7z)$/i.test(
- contentStr,
- );
-
- if (isFileLink) {
- const fileName = contentStr.split("/").pop()?.split("?")[0] || "文件";
- return (
-
-
-
📄
-
-
- {fileName.length > 20
- ? fileName.substring(0, 20) + "..."
- : fileName}
-
-
window.open(contentStr, "_blank")}
- >
- 点击查看
-
-
-
-
- );
- }
-
- // 5. 默认按文本消息处理
- return (
- {parseEmojiText(content)}
- );
+ return renderUnknownContent(rawContent, trimmedContent, msg, contract);
}
}
};
diff --git a/Touchkebao/src/store/module/weChat/weChat.data.ts b/Touchkebao/src/store/module/weChat/weChat.data.ts
index 00bc2c51..d23dfdf8 100644
--- a/Touchkebao/src/store/module/weChat/weChat.data.ts
+++ b/Touchkebao/src/store/module/weChat/weChat.data.ts
@@ -97,6 +97,11 @@ export interface WeChatState {
setVideoLoading: (messageId: number, isLoading: boolean) => void;
/** 设置视频消息URL */
setVideoUrl: (messageId: number, videoUrl: string) => void;
+ // ==================== 文件消息处理 ====================
+ /** 设置文件消息下载状态 */
+ setFileDownloading: (messageId: number, isDownloading: boolean) => void;
+ /** 设置文件消息URL */
+ setFileDownloadUrl: (messageId: number, fileUrl: string) => void;
// ==================== 消息接收处理 ====================
/** 接收新消息处理 */
diff --git a/Touchkebao/src/store/module/weChat/weChat.ts b/Touchkebao/src/store/module/weChat/weChat.ts
index d5299f47..f161bac0 100644
--- a/Touchkebao/src/store/module/weChat/weChat.ts
+++ b/Touchkebao/src/store/module/weChat/weChat.ts
@@ -27,6 +27,169 @@ let aiRequestTimer: NodeJS.Timeout | null = null;
let pendingMessages: ChatRecord[] = []; // 待处理的消息队列
let currentAiGenerationId: string | null = null; // 当前AI生成的唯一ID
const AI_REQUEST_DELAY = 3000; // 3秒延迟
+const FILE_MESSAGE_TYPE = "file";
+
+type FileMessagePayload = {
+ type?: string;
+ title?: string;
+ url?: string;
+ isDownloading?: boolean;
+ fileext?: string;
+ size?: number | string;
+ [key: string]: any;
+};
+
+const isJsonLike = (value: string) => {
+ const trimmed = value.trim();
+ return trimmed.startsWith("{") && trimmed.endsWith("}");
+};
+
+const parseFileJsonContent = (
+ rawContent: unknown,
+): FileMessagePayload | null => {
+ if (typeof rawContent !== "string") {
+ return null;
+ }
+
+ const trimmed = rawContent.trim();
+ if (!trimmed || !isJsonLike(trimmed)) {
+ return null;
+ }
+
+ try {
+ const parsed = JSON.parse(trimmed);
+ if (
+ parsed &&
+ typeof parsed === "object" &&
+ parsed.type === FILE_MESSAGE_TYPE
+ ) {
+ return parsed as FileMessagePayload;
+ }
+ } catch (error) {
+ console.warn("parseFileJsonContent failed:", error);
+ }
+
+ return null;
+};
+
+const extractFileTitleFromContent = (rawContent: unknown): string => {
+ if (typeof rawContent !== "string") {
+ return "";
+ }
+
+ const trimmed = rawContent.trim();
+ if (!trimmed) {
+ return "";
+ }
+
+ const cdataMatch =
+ trimmed.match(/<\/title>/i) ||
+ trimmed.match(/"title"\s*:\s*"([^"]+)"/i);
+ if (cdataMatch?.[1]) {
+ return cdataMatch[1].trim();
+ }
+
+ const simpleMatch = trimmed.match(/([^<]+)<\/title>/i);
+ if (simpleMatch?.[1]) {
+ return simpleMatch[1].trim();
+ }
+
+ return "";
+};
+
+const isFileLikeMessage = (msg: ChatRecord): boolean => {
+ if ((msg as any).fileDownloadMeta) {
+ return true;
+ }
+
+ if (typeof msg.content === "string") {
+ const trimmed = msg.content.trim();
+ if (!trimmed) {
+ return false;
+ }
+
+ if (
+ /"type"\s*:\s*"file"/i.test(trimmed) ||
+ / {
+ const fallbackTitle =
+ payload?.title ||
+ ((msg as any).fileDownloadMeta &&
+ typeof (msg as any).fileDownloadMeta === "object"
+ ? ((msg as any).fileDownloadMeta as FileMessagePayload).title
+ : undefined) ||
+ extractFileTitleFromContent(msg.content) ||
+ (msg as any).fileName ||
+ (msg as any).title ||
+ "";
+
+ return {
+ type: FILE_MESSAGE_TYPE,
+ ...payload,
+ title: payload?.title ?? fallbackTitle ?? "",
+ isDownloading: payload?.isDownloading ?? false,
+ };
+};
+
+const updateFileMessageState = (
+ msg: ChatRecord,
+ updater: (payload: FileMessagePayload) => FileMessagePayload,
+): ChatRecord => {
+ const parsedPayload = parseFileJsonContent(msg.content);
+
+ if (!parsedPayload && !isFileLikeMessage(msg)) {
+ return msg;
+ }
+
+ const basePayload = parsedPayload
+ ? normalizeFilePayload(parsedPayload, msg)
+ : normalizeFilePayload(
+ (msg as any).fileDownloadMeta as FileMessagePayload | undefined,
+ msg,
+ );
+
+ const updatedPayload = updater(basePayload);
+ const sanitizedPayload: FileMessagePayload = {
+ ...basePayload,
+ ...updatedPayload,
+ type: FILE_MESSAGE_TYPE,
+ title:
+ updatedPayload.title ??
+ basePayload.title ??
+ extractFileTitleFromContent(msg.content) ??
+ "",
+ isDownloading:
+ updatedPayload.isDownloading ?? basePayload.isDownloading ?? false,
+ };
+
+ if (parsedPayload) {
+ return {
+ ...msg,
+ content: JSON.stringify({
+ ...parsedPayload,
+ ...sanitizedPayload,
+ }),
+ fileDownloadMeta: sanitizedPayload,
+ };
+ }
+
+ return {
+ ...msg,
+ fileDownloadMeta: sanitizedPayload,
+ };
+};
/**
* 清除AI请求定时器和队列
@@ -618,6 +781,50 @@ export const useWeChatStore = create()(
}));
},
+ // ==================== 文件消息处理方法 ====================
+ /** 更新文件消息下载状态 */
+ setFileDownloading: (messageId: number, isDownloading: boolean) => {
+ set(state => ({
+ currentMessages: state.currentMessages.map(msg => {
+ if (msg.id !== messageId) {
+ return msg;
+ }
+
+ try {
+ return updateFileMessageState(msg, payload => ({
+ ...payload,
+ isDownloading,
+ }));
+ } catch (error) {
+ console.error("更新文件下载状态失败:", error);
+ return msg;
+ }
+ }),
+ }));
+ },
+
+ /** 更新文件消息URL */
+ setFileDownloadUrl: (messageId: number, fileUrl: string) => {
+ set(state => ({
+ currentMessages: state.currentMessages.map(msg => {
+ if (msg.id !== messageId) {
+ return msg;
+ }
+
+ try {
+ return updateFileMessageState(msg, payload => ({
+ ...payload,
+ url: fileUrl,
+ isDownloading: false,
+ }));
+ } catch (error) {
+ console.error("更新文件URL失败:", error);
+ return msg;
+ }
+ }),
+ }));
+ },
+
// ==================== 数据清理方法 ====================
/** 清空所有数据 */
clearAllData: () => {
diff --git a/Touchkebao/src/store/module/websocket/msgManage.ts b/Touchkebao/src/store/module/websocket/msgManage.ts
index 82058dc9..2fcaceb3 100644
--- a/Touchkebao/src/store/module/websocket/msgManage.ts
+++ b/Touchkebao/src/store/module/websocket/msgManage.ts
@@ -17,6 +17,8 @@ const updateMessage = useWeChatStore.getState().updateMessage;
const updateMomentCommonLoading =
useWeChatStore.getState().updateMomentCommonLoading;
const addMomentCommon = useWeChatStore.getState().addMomentCommon;
+const setFileDownloadUrl = useWeChatStore.getState().setFileDownloadUrl;
+const setFileDownloading = useWeChatStore.getState().setFileDownloading;
// 消息处理器映射
const messageHandlers: Record = {
// 微信账号存活状态响应
@@ -104,6 +106,22 @@ const messageHandlers: Record = {
console.log("视频下载结果:", message);
// setVideoUrl(message.friendMessageId, message.url);
},
+ CmdDownloadFileResult: message => {
+ const messageId = message.friendMessageId || message.chatroomMessageId;
+
+ if (!messageId) {
+ console.warn("文件下载结果缺少消息ID:", message);
+ return;
+ }
+
+ if (!message.url) {
+ console.warn("文件下载结果缺少URL:", message);
+ setFileDownloading(messageId, false);
+ return;
+ }
+
+ setFileDownloadUrl(messageId, message.url);
+ },
CmdFetchMomentResult: message => {
addMomentCommon(message.result);