messageApi.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import { apiUrl, getAgentHeaders } from "@/lib/config";
  2. import { getVisitorConversationHeaders } from "@/lib/visitor-session";
  3. import { MessageItem } from "../types";
  4. import { reportFrontendLog } from "./systemLogApi";
  5. /** 解析 POST /messages 返回的消息体(与 models.Message JSON 一致) */
  6. function messageItemFromResponse(data: unknown): MessageItem | null {
  7. if (!data || typeof data !== "object") {
  8. return null;
  9. }
  10. const raw = data as Record<string, unknown>;
  11. if (typeof raw.id !== "number" || typeof raw.conversation_id !== "number") {
  12. return null;
  13. }
  14. return {
  15. id: raw.id,
  16. conversation_id: raw.conversation_id,
  17. sender_id: typeof raw.sender_id === "number" ? raw.sender_id : 0,
  18. sender_is_agent: Boolean(raw.sender_is_agent),
  19. content: typeof raw.content === "string" ? raw.content : "",
  20. created_at:
  21. typeof raw.created_at === "string"
  22. ? raw.created_at
  23. : new Date().toISOString(),
  24. message_type:
  25. typeof raw.message_type === "string" ? raw.message_type : undefined,
  26. chat_mode: typeof raw.chat_mode === "string" ? raw.chat_mode : undefined,
  27. is_read: Boolean(raw.is_read),
  28. read_at:
  29. raw.read_at === null || raw.read_at === undefined
  30. ? null
  31. : String(raw.read_at),
  32. file_url:
  33. raw.file_url === null || raw.file_url === undefined
  34. ? null
  35. : String(raw.file_url),
  36. file_type:
  37. typeof raw.file_type === "string" ? raw.file_type : undefined,
  38. file_name:
  39. typeof raw.file_name === "string" ? raw.file_name : undefined,
  40. file_size:
  41. typeof raw.file_size === "number" ? raw.file_size : undefined,
  42. mime_type:
  43. typeof raw.mime_type === "string" ? raw.mime_type : undefined,
  44. sources_used:
  45. typeof raw.sources_used === "string" ? raw.sources_used : undefined,
  46. };
  47. }
  48. interface SendMessagePayload {
  49. conversationId: number;
  50. content: string;
  51. senderId?: number;
  52. senderIsAgent?: boolean;
  53. accessToken?: string;
  54. fileUrl?: string;
  55. fileType?: "image" | "document";
  56. fileName?: string;
  57. fileSize?: number;
  58. mimeType?: string;
  59. useKnowledgeBase?: boolean;
  60. useLLM?: boolean;
  61. useWebSearch?: boolean;
  62. needWebSearch?: boolean;
  63. }
  64. // 文件上传结果
  65. export interface UploadFileResult {
  66. file_url: string;
  67. file_type: "image" | "document";
  68. file_name: string;
  69. file_size: number;
  70. mime_type: string;
  71. }
  72. export async function fetchMessages(
  73. conversationId: number,
  74. includeAIMessages: boolean = false,
  75. accessToken?: string
  76. ): Promise<MessageItem[]> {
  77. const res = await fetch(
  78. `${apiUrl("/messages")}?conversation_id=${conversationId}&include_ai_messages=${includeAIMessages}`,
  79. {
  80. cache: "no-store",
  81. headers: {
  82. ...getAgentHeaders(),
  83. ...getVisitorConversationHeaders(conversationId, accessToken),
  84. },
  85. }
  86. );
  87. if (!res.ok) {
  88. void reportFrontendLog({
  89. level: "warn",
  90. category: "frontend",
  91. event: "fetch_messages_failed",
  92. message: "获取消息失败",
  93. conversationId,
  94. meta: { status: res.status, includeAIMessages },
  95. });
  96. throw new Error("获取消息失败");
  97. }
  98. const data = await res.json();
  99. if (!Array.isArray(data)) {
  100. return [];
  101. }
  102. return data;
  103. }
  104. // 上传文件
  105. export async function uploadFile(
  106. file: File,
  107. conversationId?: number,
  108. accessToken?: string
  109. ): Promise<UploadFileResult> {
  110. const formData = new FormData();
  111. formData.append("file", file);
  112. if (conversationId) {
  113. formData.append("conversation_id", conversationId.toString());
  114. }
  115. const res = await fetch(apiUrl("/messages/upload"), {
  116. method: "POST",
  117. headers:
  118. conversationId != null
  119. ? getVisitorConversationHeaders(conversationId, accessToken)
  120. : getAgentHeaders(),
  121. body: formData,
  122. });
  123. if (!res.ok) {
  124. const error = await res.json().catch(() => ({}));
  125. void reportFrontendLog({
  126. level: "warn",
  127. category: "frontend",
  128. event: "upload_file_failed",
  129. message: "上传文件失败",
  130. conversationId,
  131. meta: { status: res.status, error },
  132. });
  133. throw new Error(error.error || "文件上传失败");
  134. }
  135. const data = await res.json();
  136. if (!data.success) {
  137. throw new Error(data.error || "文件上传失败");
  138. }
  139. return data.data;
  140. }
  141. export async function sendMessage({
  142. conversationId,
  143. content,
  144. senderId,
  145. senderIsAgent = true,
  146. accessToken,
  147. fileUrl,
  148. fileType,
  149. fileName,
  150. fileSize,
  151. mimeType,
  152. useKnowledgeBase,
  153. useLLM,
  154. useWebSearch,
  155. needWebSearch,
  156. }: SendMessagePayload): Promise<MessageItem | null> {
  157. const payload: Record<string, unknown> = {
  158. conversation_id: conversationId,
  159. content,
  160. sender_is_agent: senderIsAgent,
  161. sender_id: typeof senderId === "number" ? senderId : 0,
  162. };
  163. if (fileUrl) {
  164. payload.file_url = fileUrl;
  165. if (fileType) payload.file_type = fileType;
  166. if (fileName) payload.file_name = fileName;
  167. if (fileSize) payload.file_size = fileSize;
  168. if (mimeType) payload.mime_type = mimeType;
  169. }
  170. if (useKnowledgeBase !== undefined) payload.use_knowledge_base = useKnowledgeBase;
  171. if (useLLM !== undefined) payload.use_llm = useLLM;
  172. if (useWebSearch !== undefined) payload.use_web_search = useWebSearch;
  173. if (needWebSearch !== undefined) payload.need_web_search = needWebSearch;
  174. const res = await fetch(apiUrl("/messages"), {
  175. method: "POST",
  176. headers: {
  177. "Content-Type": "application/json",
  178. ...getAgentHeaders(),
  179. ...(senderIsAgent
  180. ? {}
  181. : getVisitorConversationHeaders(conversationId, accessToken)),
  182. },
  183. body: JSON.stringify(payload),
  184. });
  185. if (!res.ok) {
  186. const error = await res.json().catch(() => ({}));
  187. console.error(
  188. `❌ 发送消息失败: 对话ID=${conversationId}, 状态=${res.status}, 错误=${JSON.stringify(error)}`
  189. );
  190. void reportFrontendLog({
  191. level: "error",
  192. category: "frontend",
  193. event: "send_message_failed",
  194. message: "发送消息失败",
  195. conversationId,
  196. meta: { status: res.status, error },
  197. });
  198. throw new Error(error.error || "发送消息失败");
  199. }
  200. try {
  201. const data: unknown = await res.json();
  202. return messageItemFromResponse(data);
  203. } catch {
  204. return null;
  205. }
  206. }
  207. export interface MarkMessagesReadResult {
  208. message_ids: number[];
  209. unread_count: number;
  210. read_at?: string;
  211. }
  212. export async function markMessagesRead(
  213. conversationId: number,
  214. readerIsAgent: boolean,
  215. accessToken?: string
  216. ): Promise<MarkMessagesReadResult | null> {
  217. const res = await fetch(apiUrl("/messages/read"), {
  218. method: "PUT",
  219. headers: {
  220. "Content-Type": "application/json",
  221. ...getAgentHeaders(),
  222. ...(readerIsAgent
  223. ? {}
  224. : getVisitorConversationHeaders(conversationId, accessToken)),
  225. },
  226. body: JSON.stringify({
  227. conversation_id: conversationId,
  228. reader_is_agent: readerIsAgent,
  229. }),
  230. });
  231. if (!res.ok) {
  232. return null;
  233. }
  234. const data = await res.json();
  235. return {
  236. message_ids: Array.isArray(data.message_ids) ? data.message_ids : [],
  237. unread_count:
  238. typeof data.unread_count === "number" ? data.unread_count : 0,
  239. read_at: typeof data.read_at === "string" ? data.read_at : undefined,
  240. };
  241. }