feat(chat): migrate chat to backend API
This commit is contained in:
+100
-41
@@ -1,55 +1,114 @@
|
||||
import { allow, deny } from "@/lib/decision"
|
||||
import { useAuthStore } from "@/store/auth"
|
||||
import { useChatStore } from "@/store/chat"
|
||||
import { isApiError, toApiError, type ApiDecision } from "@/lib/errors"
|
||||
import type { ChatMessage, ChatSession } from "@/lib/types"
|
||||
|
||||
export function listChatSessions() {
|
||||
return useChatStore.getState().sessions
|
||||
import { httpJson } from "./http"
|
||||
|
||||
type Paginated<T> = {
|
||||
items: T[]
|
||||
meta: {
|
||||
total: number
|
||||
offset: number
|
||||
limit: number
|
||||
}
|
||||
}
|
||||
|
||||
export function getChatSessionById(sessionId: string) {
|
||||
return useChatStore.getState().sessions.find((session) => session.id === sessionId)
|
||||
export type ListChatSessionsOptions = {
|
||||
offset?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export function listChatMessages(sessionId: string) {
|
||||
return useChatStore.getState().messages.filter((message) => message.sessionId === sessionId)
|
||||
export type ListChatMessagesOptions = {
|
||||
offset?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export function sendTextMessage(sessionId: string, content: string) {
|
||||
const userId = useAuthStore.getState().user?.id
|
||||
if (!userId) return deny(401, "请先登录")
|
||||
function withOffsetLimit(path: string, options?: { offset?: number; limit?: number }): string {
|
||||
const offset = options?.offset ?? 0
|
||||
const limit = options?.limit ?? 1000
|
||||
|
||||
const chatState = useChatStore.getState()
|
||||
const session = chatState.sessions.find((item) => item.id === sessionId)
|
||||
if (!session) return deny(404, "会话不存在")
|
||||
if (session.readonly) return deny(400, "当前会话只读")
|
||||
if (!session.participants.some((participant) => participant.id === userId)) {
|
||||
return deny(403, "仅会话参与方可发送消息")
|
||||
const searchParams = new URLSearchParams({
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
})
|
||||
|
||||
return `${path}?${searchParams.toString()}`
|
||||
}
|
||||
|
||||
function unwrapItems<T>(value: Paginated<T> | T[]): T[] {
|
||||
return Array.isArray(value) ? value : value.items
|
||||
}
|
||||
|
||||
function denyFromError(error: unknown): ApiDecision {
|
||||
if (error instanceof Error && error.message === "UNAUTHORIZED") {
|
||||
return deny(401, "请先登录")
|
||||
}
|
||||
|
||||
if (!content.trim()) {
|
||||
return deny(400, "消息不能为空")
|
||||
}
|
||||
|
||||
chatState.sendTextMessage(sessionId, userId, content)
|
||||
return allow()
|
||||
const apiError = toApiError(error)
|
||||
return deny(apiError.code, apiError.msg)
|
||||
}
|
||||
|
||||
export function sendImageMessage(sessionId: string, imageUrl: string) {
|
||||
const userId = useAuthStore.getState().user?.id
|
||||
if (!userId) return deny(401, "请先登录")
|
||||
|
||||
const chatState = useChatStore.getState()
|
||||
const session = chatState.sessions.find((item) => item.id === sessionId)
|
||||
if (!session) return deny(404, "会话不存在")
|
||||
if (session.readonly) return deny(400, "当前会话只读")
|
||||
if (!session.participants.some((participant) => participant.id === userId)) {
|
||||
return deny(403, "仅会话参与方可发送消息")
|
||||
}
|
||||
|
||||
if (!imageUrl.trim()) {
|
||||
return deny(400, "图片地址无效")
|
||||
}
|
||||
|
||||
chatState.sendImageMessage(sessionId, userId, imageUrl)
|
||||
return allow()
|
||||
export async function listChatSessions(options?: ListChatSessionsOptions): Promise<ChatSession[]> {
|
||||
const res = await httpJson<Paginated<ChatSession> | ChatSession[]>(
|
||||
withOffsetLimit("/api/v1/chat/sessions", options),
|
||||
{
|
||||
cache: "no-store",
|
||||
},
|
||||
)
|
||||
return unwrapItems(res)
|
||||
}
|
||||
|
||||
export async function getChatSessionById(sessionId: string): Promise<ChatSession | undefined> {
|
||||
try {
|
||||
return await httpJson<ChatSession>(`/api/v1/chat/sessions/${encodeURIComponent(sessionId)}`, {
|
||||
cache: "no-store",
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === "UNAUTHORIZED") {
|
||||
throw error
|
||||
}
|
||||
if (isApiError(error) && error.code === 404) {
|
||||
return undefined
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function listChatMessages(
|
||||
sessionId: string,
|
||||
options?: ListChatMessagesOptions,
|
||||
): Promise<ChatMessage[]> {
|
||||
const res = await httpJson<Paginated<ChatMessage> | ChatMessage[]>(
|
||||
withOffsetLimit(`/api/v1/chat/sessions/${encodeURIComponent(sessionId)}/messages`, options),
|
||||
{
|
||||
cache: "no-store",
|
||||
},
|
||||
)
|
||||
return unwrapItems(res)
|
||||
}
|
||||
|
||||
export async function sendTextMessage(sessionId: string, content: string): Promise<ApiDecision> {
|
||||
try {
|
||||
await httpJson<unknown>(`/api/v1/chat/sessions/${encodeURIComponent(sessionId)}/messages`, {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
json: { type: "text", content },
|
||||
})
|
||||
return allow()
|
||||
} catch (error) {
|
||||
return denyFromError(error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendImageMessage(sessionId: string, imageUrl: string): Promise<ApiDecision> {
|
||||
try {
|
||||
await httpJson<unknown>(`/api/v1/chat/sessions/${encodeURIComponent(sessionId)}/messages`, {
|
||||
method: "POST",
|
||||
cache: "no-store",
|
||||
json: { type: "image", content: imageUrl },
|
||||
})
|
||||
return allow()
|
||||
} catch (error) {
|
||||
return denyFromError(error)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user