Files
AI-CS/frontend/features/agent/services/messageApi.ts
T
2026-04-02 14:55:06 +08:00

232 lines
6.4 KiB
TypeScript

import { apiUrl, getAgentHeaders } from "@/lib/config";
import { MessageItem } from "../types";
import { reportFrontendLog } from "./systemLogApi";
/** 解析 POST /messages 返回的消息体(与 models.Message JSON 一致) */
function messageItemFromResponse(data: unknown): MessageItem | null {
if (!data || typeof data !== "object") {
return null;
}
const raw = data as Record<string, unknown>;
if (typeof raw.id !== "number" || typeof raw.conversation_id !== "number") {
return null;
}
return {
id: raw.id,
conversation_id: raw.conversation_id,
sender_id: typeof raw.sender_id === "number" ? raw.sender_id : 0,
sender_is_agent: Boolean(raw.sender_is_agent),
content: typeof raw.content === "string" ? raw.content : "",
created_at:
typeof raw.created_at === "string"
? raw.created_at
: new Date().toISOString(),
message_type:
typeof raw.message_type === "string" ? raw.message_type : undefined,
chat_mode: typeof raw.chat_mode === "string" ? raw.chat_mode : undefined,
is_read: Boolean(raw.is_read),
read_at:
raw.read_at === null || raw.read_at === undefined
? null
: String(raw.read_at),
file_url:
raw.file_url === null || raw.file_url === undefined
? null
: String(raw.file_url),
file_type:
typeof raw.file_type === "string" ? raw.file_type : undefined,
file_name:
typeof raw.file_name === "string" ? raw.file_name : undefined,
file_size:
typeof raw.file_size === "number" ? raw.file_size : undefined,
mime_type:
typeof raw.mime_type === "string" ? raw.mime_type : undefined,
sources_used:
typeof raw.sources_used === "string" ? raw.sources_used : undefined,
};
}
interface SendMessagePayload {
conversationId: number;
content: string;
senderId?: number;
senderIsAgent?: boolean;
fileUrl?: string;
fileType?: "image" | "document";
fileName?: string;
fileSize?: number;
mimeType?: string;
useKnowledgeBase?: boolean;
useLLM?: boolean;
useWebSearch?: boolean;
needWebSearch?: boolean;
}
// 文件上传结果
export interface UploadFileResult {
file_url: string;
file_type: "image" | "document";
file_name: string;
file_size: number;
mime_type: string;
}
export async function fetchMessages(
conversationId: number,
includeAIMessages: boolean = false
): Promise<MessageItem[]> {
const res = await fetch(
`${apiUrl("/messages")}?conversation_id=${conversationId}&include_ai_messages=${includeAIMessages}`,
{
cache: "no-store",
}
);
if (!res.ok) {
void reportFrontendLog({
level: "warn",
category: "frontend",
event: "fetch_messages_failed",
message: "获取消息失败",
conversationId,
meta: { status: res.status, includeAIMessages },
});
throw new Error("获取消息失败");
}
const data = await res.json();
if (!Array.isArray(data)) {
return [];
}
return data;
}
// 上传文件
export async function uploadFile(
file: File,
conversationId?: number
): Promise<UploadFileResult> {
const formData = new FormData();
formData.append("file", file);
if (conversationId) {
formData.append("conversation_id", conversationId.toString());
}
const res = await fetch(apiUrl("/messages/upload"), {
method: "POST",
body: formData,
});
if (!res.ok) {
const error = await res.json().catch(() => ({}));
void reportFrontendLog({
level: "warn",
category: "frontend",
event: "upload_file_failed",
message: "上传文件失败",
conversationId,
meta: { status: res.status, error },
});
throw new Error(error.error || "文件上传失败");
}
const data = await res.json();
if (!data.success) {
throw new Error(data.error || "文件上传失败");
}
return data.data;
}
export async function sendMessage({
conversationId,
content,
senderId,
senderIsAgent = true,
fileUrl,
fileType,
fileName,
fileSize,
mimeType,
useKnowledgeBase,
useLLM,
useWebSearch,
needWebSearch,
}: SendMessagePayload): Promise<MessageItem | null> {
const payload: Record<string, unknown> = {
conversation_id: conversationId,
content,
sender_is_agent: senderIsAgent,
sender_id: typeof senderId === "number" ? senderId : 0,
};
if (fileUrl) {
payload.file_url = fileUrl;
if (fileType) payload.file_type = fileType;
if (fileName) payload.file_name = fileName;
if (fileSize) payload.file_size = fileSize;
if (mimeType) payload.mime_type = mimeType;
}
if (useKnowledgeBase !== undefined) payload.use_knowledge_base = useKnowledgeBase;
if (useLLM !== undefined) payload.use_llm = useLLM;
if (useWebSearch !== undefined) payload.use_web_search = useWebSearch;
if (needWebSearch !== undefined) payload.need_web_search = needWebSearch;
const res = await fetch(apiUrl("/messages"), {
method: "POST",
headers: { "Content-Type": "application/json", ...getAgentHeaders() },
body: JSON.stringify(payload),
});
if (!res.ok) {
const error = await res.json().catch(() => ({}));
console.error(
`❌ 发送消息失败: 对话ID=${conversationId}, 状态=${res.status}, 错误=${JSON.stringify(error)}`
);
void reportFrontendLog({
level: "error",
category: "frontend",
event: "send_message_failed",
message: "发送消息失败",
conversationId,
meta: { status: res.status, error },
});
throw new Error(error.error || "发送消息失败");
}
try {
const data: unknown = await res.json();
return messageItemFromResponse(data);
} catch {
return null;
}
}
export interface MarkMessagesReadResult {
message_ids: number[];
unread_count: number;
read_at?: string;
}
export async function markMessagesRead(
conversationId: number,
readerIsAgent: boolean
): Promise<MarkMessagesReadResult | null> {
const res = await fetch(apiUrl("/messages/read"), {
method: "PUT",
headers: { "Content-Type": "application/json", ...getAgentHeaders() },
body: JSON.stringify({
conversation_id: conversationId,
reader_is_agent: readerIsAgent,
}),
});
if (!res.ok) {
return null;
}
const data = await res.json();
return {
message_ids: Array.isArray(data.message_ids) ? data.message_ids : [],
unread_count:
typeof data.unread_count === "number" ? data.unread_count : 0,
read_at: typeof data.read_at === "string" ? data.read_at : undefined,
};
}