messageApi.ts 6.4 KB

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