feat(chat): desktop usando Convex WS direto e fallback WS dedicado
This commit is contained in:
parent
8db7c3c810
commit
a8f5ff9d51
14 changed files with 735 additions and 458 deletions
180
apps/desktop/src/chat/convexMachineClient.ts
Normal file
180
apps/desktop/src/chat/convexMachineClient.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import { ConvexClient } from "convex/browser"
|
||||
import { Store } from "@tauri-apps/plugin-store"
|
||||
import { appLocalDataDir, join } from "@tauri-apps/api/path"
|
||||
import type { ChatMessage } from "./types"
|
||||
|
||||
const STORE_FILENAME = "machine-agent.json"
|
||||
const DEFAULT_CONVEX_URL =
|
||||
import.meta.env.VITE_CONVEX_URL?.trim() ||
|
||||
"https://convex.esdrasrenan.com.br"
|
||||
|
||||
type MachineStoreConfig = {
|
||||
apiBaseUrl?: string
|
||||
appUrl?: string
|
||||
convexUrl?: string
|
||||
}
|
||||
|
||||
type MachineStoreData = {
|
||||
token?: string
|
||||
config?: MachineStoreConfig
|
||||
}
|
||||
|
||||
type ClientCache = {
|
||||
client: ConvexClient
|
||||
token: string
|
||||
convexUrl: string
|
||||
}
|
||||
|
||||
let cached: ClientCache | null = null
|
||||
|
||||
type MachineUpdatePayload = {
|
||||
hasActiveSessions: boolean
|
||||
sessions: Array<{ ticketId: string; unreadCount: number; lastActivityAt: number }>
|
||||
totalUnread: number
|
||||
}
|
||||
|
||||
async function loadStore(): Promise<MachineStoreData> {
|
||||
const appData = await appLocalDataDir()
|
||||
const storePath = await join(appData, STORE_FILENAME)
|
||||
const store = await Store.load(storePath)
|
||||
const token = await store.get<string>("token")
|
||||
const config = await store.get<MachineStoreConfig>("config")
|
||||
return { token: token ?? undefined, config: config ?? undefined }
|
||||
}
|
||||
|
||||
function resolveConvexUrl(config?: MachineStoreConfig): string {
|
||||
const fromConfig = config?.convexUrl?.trim()
|
||||
if (fromConfig) return fromConfig.replace(/\/+$/, "")
|
||||
return DEFAULT_CONVEX_URL
|
||||
}
|
||||
|
||||
function resolveApiBaseUrl(config?: MachineStoreConfig): string {
|
||||
const fromConfig = config?.apiBaseUrl?.trim()
|
||||
if (fromConfig) return fromConfig.replace(/\/+$/, "")
|
||||
return "https://tickets.esdrasrenan.com.br"
|
||||
}
|
||||
|
||||
export async function getMachineStoreConfig() {
|
||||
const data = await loadStore()
|
||||
if (!data.token) {
|
||||
throw new Error("Token de máquina não encontrado no store")
|
||||
}
|
||||
const apiBaseUrl = resolveApiBaseUrl(data.config)
|
||||
const appUrl = data.config?.appUrl?.trim() || apiBaseUrl
|
||||
return { token: data.token, apiBaseUrl, appUrl, convexUrl: resolveConvexUrl(data.config) }
|
||||
}
|
||||
|
||||
async function ensureClient(): Promise<ClientCache> {
|
||||
const data = await loadStore()
|
||||
if (!data.token) {
|
||||
throw new Error("Token de máquina não encontrado no store")
|
||||
}
|
||||
const convexUrl = resolveConvexUrl(data.config)
|
||||
|
||||
if (cached && cached.token === data.token && cached.convexUrl === convexUrl) {
|
||||
return cached
|
||||
}
|
||||
|
||||
const client = new ConvexClient(convexUrl)
|
||||
cached = { client, token: data.token, convexUrl }
|
||||
return cached
|
||||
}
|
||||
|
||||
export async function subscribeMachineUpdates(
|
||||
callback: (payload: MachineUpdatePayload) => void,
|
||||
onError?: (error: Error) => void
|
||||
): Promise<() => void> {
|
||||
const { client, token } = await ensureClient()
|
||||
const sub = client.onUpdate(
|
||||
FN_CHECK_UPDATES as any,
|
||||
{ machineToken: token },
|
||||
(value) => callback(value),
|
||||
onError
|
||||
)
|
||||
return () => sub.unsubscribe()
|
||||
}
|
||||
|
||||
export async function subscribeMachineMessages(
|
||||
ticketId: string,
|
||||
callback: (payload: { messages: ChatMessage[]; hasSession: boolean }) => void,
|
||||
onError?: (error: Error) => void
|
||||
): Promise<() => void> {
|
||||
const { client, token } = await ensureClient()
|
||||
const sub = client.onUpdate(
|
||||
FN_LIST_MESSAGES as any,
|
||||
{
|
||||
machineToken: token,
|
||||
ticketId,
|
||||
},
|
||||
(value) => callback(value),
|
||||
onError
|
||||
)
|
||||
return () => sub.unsubscribe()
|
||||
}
|
||||
|
||||
export async function sendMachineMessage(input: {
|
||||
ticketId: string
|
||||
body: string
|
||||
attachments?: Array<{
|
||||
storageId: string
|
||||
name: string
|
||||
size?: number
|
||||
type?: string
|
||||
}>
|
||||
}) {
|
||||
const { client, token } = await ensureClient()
|
||||
return client.mutation(FN_POST_MESSAGE as any, {
|
||||
machineToken: token,
|
||||
ticketId: input.ticketId,
|
||||
body: input.body,
|
||||
attachments: input.attachments?.map((att) => ({
|
||||
storageId: att.storageId,
|
||||
name: att.name,
|
||||
size: att.size,
|
||||
type: att.type,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
export async function markMachineMessagesRead(ticketId: string, messageIds: string[]) {
|
||||
if (messageIds.length === 0) return
|
||||
const { client, token } = await ensureClient()
|
||||
await client.mutation(FN_MARK_READ as any, {
|
||||
machineToken: token,
|
||||
ticketId,
|
||||
messageIds,
|
||||
})
|
||||
}
|
||||
|
||||
export async function generateMachineUploadUrl(opts: {
|
||||
fileName: string
|
||||
fileType: string
|
||||
fileSize: number
|
||||
}) {
|
||||
const { client, token } = await ensureClient()
|
||||
return client.action(FN_UPLOAD_URL as any, {
|
||||
machineToken: token,
|
||||
fileName: opts.fileName,
|
||||
fileType: opts.fileType,
|
||||
fileSize: opts.fileSize,
|
||||
})
|
||||
}
|
||||
|
||||
export async function uploadToConvexStorage(uploadUrl: string, file: Blob | ArrayBuffer, contentType: string) {
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": contentType },
|
||||
body: file,
|
||||
})
|
||||
if (!response.ok) {
|
||||
const body = await response.text()
|
||||
throw new Error(`Upload falhou: ${response.status} ${body}`)
|
||||
}
|
||||
const json = await response.json().catch(() => ({}))
|
||||
return json.storageId || json.storage_id
|
||||
}
|
||||
const FN_CHECK_UPDATES = "liveChat.checkMachineUpdates"
|
||||
const FN_LIST_MESSAGES = "liveChat.listMachineMessages"
|
||||
const FN_POST_MESSAGE = "liveChat.postMachineMessage"
|
||||
const FN_MARK_READ = "liveChat.markMachineMessagesRead"
|
||||
const FN_UPLOAD_URL = "liveChat.generateMachineUploadUrl"
|
||||
Loading…
Add table
Add a link
Reference in a new issue