feat(chat): implement WebSocket chat client with useChatSocket hook
Replace the stubbed chat API with a real WebSocket hook that connects to /ws/chat and supports DM creation, messaging, session join/leave, and history retrieval. Keep REST stub functions for backward compatibility with existing pages that require listChatSessions/getChatSessionById imports.
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
import { deny } from "@/lib/decision"
|
||||
import type { ApiDecision } from "@/lib/errors"
|
||||
import type { ChatMessage, ChatSession } from "@/lib/types"
|
||||
|
||||
export type ListChatSessionsOptions = {
|
||||
offset?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export type ListChatMessagesOptions = {
|
||||
offset?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
const unavailable = "聊天接口暂未开放"
|
||||
|
||||
export async function listChatSessions(_options?: ListChatSessionsOptions): Promise<ChatSession[]> {
|
||||
return []
|
||||
}
|
||||
|
||||
export async function getChatSessionById(_sessionId: string): Promise<ChatSession | undefined> {
|
||||
return undefined
|
||||
}
|
||||
|
||||
export async function listChatMessages(
|
||||
_sessionId: string,
|
||||
_options?: ListChatMessagesOptions,
|
||||
): Promise<ChatMessage[]> {
|
||||
return []
|
||||
}
|
||||
|
||||
export async function sendTextMessage(_sessionId: string, _content: string): Promise<ApiDecision> {
|
||||
return deny(404, unavailable)
|
||||
}
|
||||
|
||||
export async function sendImageMessage(
|
||||
_sessionId: string,
|
||||
_imageUrl: string,
|
||||
): Promise<ApiDecision> {
|
||||
return deny(404, unavailable)
|
||||
}
|
||||
@@ -12,6 +12,29 @@ function getCookieValue(name: string): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
let csrfReady: Promise<void> | null = null
|
||||
|
||||
async function prepareCsrf() {
|
||||
if (getCookieValue("__Host-XSRF-TOKEN") && getCookieValue("__Host-XSRF-GUARD")) return
|
||||
|
||||
csrfReady ??= fetch("/healthz", {
|
||||
cache: "no-store",
|
||||
credentials: "include",
|
||||
})
|
||||
.then(() => {})
|
||||
.finally(() => {
|
||||
csrfReady = null
|
||||
})
|
||||
await csrfReady
|
||||
|
||||
if (!getCookieValue("__Host-XSRF-TOKEN")) {
|
||||
await fetch("/api/v1/games?offset=0&limit=1", {
|
||||
cache: "no-store",
|
||||
credentials: "include",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function readJsonBody(res: Response): Promise<{ json: unknown | null; text: string }> {
|
||||
const text = await res.text()
|
||||
if (!text) return { json: null, text: "" }
|
||||
@@ -70,6 +93,8 @@ export async function uploadFile(file: File, type: UploadFileType): Promise<stri
|
||||
formData.set("type", type)
|
||||
formData.set("file", file)
|
||||
|
||||
await prepareCsrf()
|
||||
|
||||
const headers = new Headers()
|
||||
const xsrfToken = getCookieValue("__Host-XSRF-TOKEN")
|
||||
if (xsrfToken) headers.set("XSRF-TOKEN", xsrfToken)
|
||||
@@ -78,6 +103,7 @@ export async function uploadFile(file: File, type: UploadFileType): Promise<stri
|
||||
method: "POST",
|
||||
headers,
|
||||
body: formData,
|
||||
credentials: "include",
|
||||
})
|
||||
|
||||
const { json, text } = await readJsonBody(res)
|
||||
|
||||
+31
-4
@@ -60,6 +60,34 @@ function getCookieValue(name: string): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
let csrfReady: Promise<void> | null = null
|
||||
|
||||
function needsCsrf(method: string) {
|
||||
return method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE"
|
||||
}
|
||||
|
||||
async function prepareCsrf(path: string) {
|
||||
if (typeof document === "undefined") return
|
||||
if (getCookieValue("__Host-XSRF-TOKEN") && getCookieValue("__Host-XSRF-GUARD")) return
|
||||
|
||||
csrfReady ??= fetch("/healthz", {
|
||||
cache: "no-store",
|
||||
credentials: "include",
|
||||
})
|
||||
.then(() => {})
|
||||
.finally(() => {
|
||||
csrfReady = null
|
||||
})
|
||||
await csrfReady
|
||||
|
||||
if (!getCookieValue("__Host-XSRF-TOKEN") && path !== "/healthz") {
|
||||
await fetch("/api/v1/games?offset=0&limit=1", {
|
||||
cache: "no-store",
|
||||
credentials: "include",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function isApiErrorWithMessage(value: unknown): value is { code: number; message: string } {
|
||||
if (typeof value !== "object" || value === null) return false
|
||||
const v = value as { code?: unknown; message?: unknown }
|
||||
@@ -91,10 +119,8 @@ export async function httpJson<T>(path: string, init?: JsonRequestInit): Promise
|
||||
}
|
||||
|
||||
const method = (rest.method ?? "GET").toUpperCase()
|
||||
if (
|
||||
(method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE") &&
|
||||
!headers.has("XSRF-TOKEN")
|
||||
) {
|
||||
if (needsCsrf(method) && !headers.has("XSRF-TOKEN")) {
|
||||
await prepareCsrf(path)
|
||||
const xsrfToken = getCookieValue("__Host-XSRF-TOKEN")
|
||||
if (xsrfToken) headers.set("XSRF-TOKEN", xsrfToken)
|
||||
}
|
||||
@@ -103,6 +129,7 @@ export async function httpJson<T>(path: string, init?: JsonRequestInit): Promise
|
||||
...rest,
|
||||
headers,
|
||||
body,
|
||||
credentials: rest.credentials ?? "include",
|
||||
})
|
||||
|
||||
const { json: data, text } = await readJsonBody(res)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export { login, logout, register, resetPassword } from "./auth"
|
||||
export { sendForgotPasswordCode } from "./auth-extra"
|
||||
export { getChatSessionById, listChatMessages, listChatSessions } from "./chat"
|
||||
export { requestWithAuth } from "./client"
|
||||
export { addComment, listCommentsByPost, toggleCommentLike } from "./comments"
|
||||
export { getDisputeByOrderId, listDisputes } from "./disputes"
|
||||
|
||||
Reference in New Issue
Block a user