feat(chat): migrate chat to backend API

This commit is contained in:
zetaloop
2026-03-01 17:03:30 +08:00
parent f189ec9846
commit e2671638e6
6 changed files with 223 additions and 76 deletions
+100 -41
View File
@@ -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)
}
}