import type { ConfirmTaskPayload, CreateTaskInput, PlatformId } from "@cross-ai/domain"; import cors from "@fastify/cors"; import Fastify from "fastify"; import { JdLiveSessionService, isJdLiveError } from "./platforms/jd/live-session"; import type { JdLiveService, JdSearchMode } from "./platforms/jd/types"; import { InMemoryTaskStore } from "./store"; export function createServer(options: { jdLiveService?: JdLiveService } = {}) { const app = Fastify({ logger: false }); const store = new InMemoryTaskStore(); const jdLiveService = options.jdLiveService ?? new JdLiveSessionService(); app.register(cors, { origin: true }); app.get("/api/health", async () => ({ status: "ok", service: "cross-platform-products-ai-analyst-api" })); app.get("/api/platforms/readiness", async () => ({ platforms: store.getPlatformReadiness() })); app.get("/api/sessions", async () => ({ sessions: store.listSessions() })); app.get<{ Params: { platform: PlatformId }; }>("/api/sessions/:platform", async (request, reply) => { try { const session = store.getSession(request.params.platform); return { session }; } catch { reply.code(404); return { message: "Session not found." }; } }); app.post<{ Params: { platform: PlatformId }; }>("/api/platforms/:platform/prepare", async (request, reply) => { const session = store.preparePlatform(request.params.platform); reply.code(200); return { platform: session.platform, session_ready: session.ready, status: session.status, last_prepared_at: session.lastPreparedAt, expires_at: session.expiresAt, encrypted_snapshot_available: session.encryptedSnapshotAvailable }; }); app.get("/api/platforms/jd/live-session", async () => ({ session: jdLiveService.getSessionSummary() })); app.post<{ Body: { cookieHeader: string; userAgent?: string; searchApiTemplateUrl?: string; detailTemplateUrl?: string; reviewsTemplateUrl?: string; searchReferer?: string; detailReferer?: string; }; }>("/api/platforms/jd/live-session", async (request, reply) => { try { const session = jdLiveService.importSession(request.body); store.preparePlatform("jd"); reply.code(200); return { session }; } catch (error) { reply.code(isJdLiveError(error) ? error.statusCode : 400); return { message: error instanceof Error ? error.message : "Invalid JD live session payload." }; } }); app.delete("/api/platforms/jd/live-session", async (_request, reply) => { jdLiveService.clearSession(); store.clearPlatformSession("jd"); reply.code(204); return null; }); app.delete<{ Params: { platform: PlatformId }; }>("/api/sessions/:platform", async (request, reply) => { try { store.clearPlatformSession(request.params.platform); if (request.params.platform === "jd") { jdLiveService.clearSession(); } reply.code(204); return null; } catch { reply.code(404); return { message: "Session not found." }; } }); app.post<{ Body: CreateTaskInput; }>("/api/tasks", async (request, reply) => { const task = store.createTask(request.body); reply.code(201); return { task }; }); app.get<{ Params: { taskId: string }; }>("/api/tasks/:taskId", async (request, reply) => { const task = store.getTask(request.params.taskId); if (!task) { reply.code(404); return { message: "Task not found." }; } return { task }; }); app.delete<{ Params: { taskId: string }; }>("/api/tasks/:taskId", async (request, reply) => { try { store.deleteTask(request.params.taskId); reply.code(204); return null; } catch { reply.code(404); return { message: "Task not found." }; } }); app.get<{ Params: { taskId: string }; }>("/api/tasks/:taskId/candidates", async (request, reply) => { const candidates = store.getCandidates(request.params.taskId); if (!candidates) { reply.code(404); return { message: "Task not found." }; } return { candidates }; }); app.get<{ Params: { taskId: string }; }>("/api/tasks/:taskId/strategy-attempts", async (request, reply) => { const attempts = store.getTaskStrategyAttempts(request.params.taskId); if (!attempts) { reply.code(404); return { message: "Task not found." }; } return { attempts }; }); app.get<{ Params: { taskId: string }; }>("/api/tasks/:taskId/audit", async (request, reply) => { const audit = store.getTaskAuditLogs(request.params.taskId); if (!audit) { reply.code(404); return { message: "Task not found." }; } return { audit }; }); app.post<{ Params: { taskId: string }; Body: ConfirmTaskPayload; }>("/api/tasks/:taskId/confirm", async (request, reply) => { try { const task = store.confirmTask(request.params.taskId, request.body); return { task }; } catch { reply.code(404); return { message: "Task not found." }; } }); app.post<{ Params: { taskId: string; platform: PlatformId }; }>("/api/tasks/:taskId/platforms/:platform/retry", async (request, reply) => { try { const task = store.retryPlatform(request.params.taskId, request.params.platform); return { task }; } catch { reply.code(404); return { message: "Task not found." }; } }); app.get<{ Params: { taskId: string }; Querystring: { version?: string }; }>("/api/tasks/:taskId/report", async (request, reply) => { const version = request.query.version ? Number(request.query.version) : undefined; const report = store.getReport(request.params.taskId, version); if (!report) { reply.code(404); return { message: "Report not found." }; } return { report }; }); app.get<{ Querystring: { query?: string; mode?: JdSearchMode }; }>("/api/platforms/jd/live-search-preview", async (request, reply) => { try { const query = request.query.query?.trim(); if (!query) { reply.code(400); return { message: "query is required." }; } const preview = await jdLiveService.previewSearch(query, request.query.mode); return { preview }; } catch (error) { reply.code(isJdLiveError(error) ? error.statusCode : 502); return { message: error instanceof Error ? error.message : "JD live search preview failed." }; } }); app.get<{ Querystring: { skuId?: string }; }>("/api/platforms/jd/live-detail-preview", async (request, reply) => { try { const skuId = request.query.skuId?.trim(); if (!skuId) { reply.code(400); return { message: "skuId is required." }; } const preview = await jdLiveService.previewDetail(skuId); return { preview }; } catch (error) { reply.code(isJdLiveError(error) ? error.statusCode : 502); return { message: error instanceof Error ? error.message : "JD live detail preview failed." }; } }); app.get<{ Querystring: { skuId?: string; commentCount?: string }; }>("/api/platforms/jd/live-reviews-preview", async (request, reply) => { try { const skuId = request.query.skuId?.trim(); if (!skuId) { reply.code(400); return { message: "skuId is required." }; } const commentCount = request.query.commentCount ? Number.parseInt(request.query.commentCount, 10) : undefined; const preview = await jdLiveService.previewReviews(skuId, commentCount); return { preview }; } catch (error) { reply.code(isJdLiveError(error) ? error.statusCode : 502); return { message: error instanceof Error ? error.message : "JD live reviews preview failed." }; } }); app.get("/api/history", async () => ({ tasks: store.listHistory() })); app.get("/api/observability/overview", async () => ({ overview: store.getObservabilityOverview() })); app.get<{ Params: { taskId: string }; }>("/api/tasks/:taskId/events", async (request, reply) => { const task = store.getTask(request.params.taskId); if (!task) { reply.code(404); return { message: "Task not found." }; } reply.raw.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" }); reply.raw.write(`event: task.snapshot\n`); reply.raw.write(`data: ${JSON.stringify(task)}\n\n`); reply.raw.end(); return reply; }); return app; }