import type { ConfirmTaskPayload, CreateTaskInput, PlatformId } from "@cross-ai/domain"; import cors from "@fastify/cors"; import Fastify from "fastify"; import { resolve } from "node:path"; import { rankJdCandidatesForKeyword } from "./platforms/jd/keyword-preview"; import { JdLiveSessionService, isJdLiveError } from "./platforms/jd/live-session"; import { JdSessionManagerService } from "./platforms/jd/session-manager"; import type { JdLiveService, JdSearchMode, JdSessionManager, JdSessionManagerAutoMode } from "./platforms/jd/types"; import { JdOpsQrLoginService, TmallOpsQrLoginService, type OpsQrLoginController } from "./ops/qr-login"; import { TmallLiveSessionService, isTmallLiveError } from "./platforms/tmall/live-session"; import { TmallSessionManagerService } from "./platforms/tmall/session-manager"; import type { TmallLiveService, TmallSessionManager, TmallSessionManagerConfigInput } from "./platforms/tmall/types"; import { InMemoryTaskStore } from "./store"; export function createServer( options: { storagePath?: string; jdLiveService?: JdLiveService; jdSessionManager?: JdSessionManager; jdQrLoginService?: OpsQrLoginController; tmallLiveService?: TmallLiveService; tmallSessionManager?: TmallSessionManager; tmallQrLoginService?: OpsQrLoginController; executionMode?: "synchronous" | "background"; } = {} ) { const app = Fastify({ logger: false }); const jdLiveService = options.jdLiveService ?? new JdLiveSessionService(); const tmallLiveService = options.tmallLiveService ?? new TmallLiveSessionService(); const storagePath = options.storagePath ?? process.env.TASK_STORE_PATH ?? (process.env.NODE_ENV === "test" ? undefined : resolve(process.cwd(), ".data", "task-store.json")); const store = new InMemoryTaskStore({ storagePath, jdLiveService, tmallLiveService, executionMode: options.executionMode }); const jdSessionManager = options.jdSessionManager ?? new JdSessionManagerService(jdLiveService, { onSessionReady: () => { store.notifyManagedSessionReady("jd"); }, onSessionUnavailable: () => { store.clearPlatformSession("jd"); } }); const tmallSessionManager = options.tmallSessionManager ?? new TmallSessionManagerService(tmallLiveService, { onSessionReady: () => { store.preparePlatform("tmall"); }, onSessionUnavailable: () => { store.clearPlatformSession("tmall"); } }); const jdQrLoginService = options.jdQrLoginService ?? new JdOpsQrLoginService(jdSessionManager, { resolveSearchQuery: () => jdSessionManager.getState().heartbeatQuery }); const tmallQrLoginService = options.tmallQrLoginService ?? new TmallOpsQrLoginService(tmallSessionManager, { resolveTargetItemId: () => tmallSessionManager.getState().heartbeatItemId }); if ( jdLiveService instanceof JdLiveSessionService && jdQrLoginService instanceof JdOpsQrLoginService ) { jdLiveService.setBrowserPreviewProvider(jdQrLoginService); } store.setJdSessionManager(jdSessionManager); store.setTmallSessionManager(tmallSessionManager); if (jdLiveService.getSessionSummary().configured && !store.getSession("jd").ready) { store.notifyManagedSessionReady("jd"); } if (tmallLiveService.getSessionSummary().configured && !store.getSession("tmall").ready) { store.preparePlatform("tmall"); } app.addHook("onClose", async () => { jdSessionManager.shutdown(); tmallSessionManager.shutdown(); await jdQrLoginService.shutdown(); await tmallQrLoginService.shutdown(); }); 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().map((platform) => platform.platform === "jd" ? { ...platform, reason: jdSessionManager.getState().publicNote } : platform.platform === "tmall" ? { ...platform, reason: tmallSessionManager.getState().publicNote } : platform ) })); 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.get("/api/ops/jd/session-manager", async () => ({ manager: jdSessionManager.getState() })); app.post<{ Body: { enabled?: boolean; autoLoginMode?: JdSessionManagerAutoMode; loginCommand?: string | null; browserProfilePath?: string | null; heartbeatQuery?: string | null; account?: string | null; password?: string | null; checkIntervalMs?: number | null; runnerTimeoutMs?: number | null; }; }>("/api/ops/jd/session-manager/config", async (request, reply) => { try { const manager = jdSessionManager.configure(request.body); reply.code(200); return { manager }; } catch (error) { reply.code(400); return { message: error instanceof Error ? error.message : "Invalid JD ops session manager config." }; } }); app.delete("/api/ops/jd/session-manager/config", async () => ({ manager: jdSessionManager.clearConfig() })); app.post("/api/ops/jd/session-manager/check", async (_request, reply) => { try { const result = await jdSessionManager.runHealthCheck("ops"); reply.code(200); return result; } catch (error) { reply.code(502); return { message: error instanceof Error ? error.message : "JD ops health check failed." }; } }); app.post("/api/ops/jd/session-manager/recover", async (_request, reply) => { try { const result = await jdSessionManager.runAutoRecovery("ops"); reply.code(200); return result; } catch (error) { reply.code(502); return { message: error instanceof Error ? error.message : "JD ops auto recovery failed." }; } }); app.post<{ Body: { cookieHeader: string; userAgent?: string; searchApiTemplateUrl?: string; detailTemplateUrl?: string; reviewsTemplateUrl?: string; searchReferer?: string; detailReferer?: string; }; }>("/api/ops/jd/session-manager/session", async (request, reply) => { try { await jdSessionManager.importManualSession(request.body, "ops-manual"); const result = await jdSessionManager.runHealthCheck("ops-manual"); reply.code(200); return { manager: result.state, recovered: result.recovered }; } catch (error) { reply.code(isJdLiveError(error) ? error.statusCode : 400); return { message: error instanceof Error ? error.message : "Invalid JD ops live session payload." }; } }); app.delete("/api/ops/jd/session-manager/session", async () => ({ manager: jdSessionManager.clearManagedSession("ops-manual-clear") })); app.get("/api/ops/jd/session-manager/qr-login", async () => ({ qrLogin: jdQrLoginService.getState() })); app.post("/api/ops/jd/session-manager/qr-login/start", async (_request, reply) => { try { const qrLogin = await jdQrLoginService.start(); reply.code(200); return { qrLogin }; } catch (error) { reply.code(500); return { message: error instanceof Error ? error.message : "JD QR login start failed." }; } }); app.post("/api/ops/jd/session-manager/qr-login/cancel", async (_request, reply) => { const qrLogin = await jdQrLoginService.cancel("ops-cancel"); reply.code(200); return { qrLogin }; }); app.post("/api/ops/jd/session-manager/qr-login/resume", async (_request, reply) => { try { const qrLogin = await jdQrLoginService.resumeManualRecovery(); reply.code(200); return { qrLogin }; } catch (error) { reply.code(500); return { message: error instanceof Error ? error.message : "JD QR login manual recovery failed." }; } }); app.get("/api/ops/tmall/session-manager", async () => ({ manager: tmallSessionManager.getState() })); app.post<{ Body: TmallSessionManagerConfigInput; }>("/api/ops/tmall/session-manager/config", async (request, reply) => { try { const manager = tmallSessionManager.configure(request.body); reply.code(200); return { manager }; } catch (error) { reply.code(400); return { message: error instanceof Error ? error.message : "Invalid Tmall ops session manager config." }; } }); app.delete("/api/ops/tmall/session-manager/config", async () => ({ manager: tmallSessionManager.clearConfig() })); app.post("/api/ops/tmall/session-manager/check", async (_request, reply) => { try { const result = await tmallSessionManager.runHealthCheck("ops"); reply.code(200); return result; } catch (error) { reply.code(502); return { message: error instanceof Error ? error.message : "Tmall ops health check failed." }; } }); app.post<{ Body: { cookieHeader: string; userAgent?: string; detailTemplateUrl?: string; reviewsTemplateUrl?: string; detailReferer?: string; }; }>("/api/ops/tmall/session-manager/session", async (request, reply) => { try { const manager = await tmallSessionManager.importManualSession(request.body, "ops-manual"); reply.code(200); return { manager }; } catch (error) { reply.code(isTmallLiveError(error) ? error.statusCode : 400); return { message: error instanceof Error ? error.message : "Invalid Tmall ops live session payload." }; } }); app.delete("/api/ops/tmall/session-manager/session", async () => ({ manager: tmallSessionManager.clearManagedSession("ops-manual-clear") })); app.get("/api/ops/tmall/session-manager/qr-login", async () => ({ qrLogin: tmallQrLoginService.getState() })); app.post("/api/ops/tmall/session-manager/qr-login/start", async (_request, reply) => { try { const qrLogin = await tmallQrLoginService.start(); reply.code(200); return { qrLogin }; } catch (error) { reply.code(500); return { message: error instanceof Error ? error.message : "Tmall QR login start failed." }; } }); app.post("/api/ops/tmall/session-manager/qr-login/cancel", async (_request, reply) => { const qrLogin = await tmallQrLoginService.cancel("ops-cancel"); reply.code(200); return { qrLogin }; }); app.get("/api/platforms/tmall/live-session", async () => ({ session: tmallLiveService.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 { await jdSessionManager.importManualSession(request.body, "legacy-live-session"); await jdSessionManager.runHealthCheck("legacy-live-session"); reply.code(200); return { session: jdLiveService.getSessionSummary() }; } catch (error) { reply.code(isJdLiveError(error) ? error.statusCode : 400); return { message: error instanceof Error ? error.message : "Invalid JD live session payload." }; } }); app.post<{ Body: { cookieHeader: string; userAgent?: string; detailTemplateUrl?: string; reviewsTemplateUrl?: string; detailReferer?: string; }; }>("/api/platforms/tmall/live-session", async (request, reply) => { try { await tmallSessionManager.importManualSession(request.body, "legacy-live-session"); const session = tmallLiveService.getSessionSummary(); reply.code(200); return { session }; } catch (error) { reply.code(isTmallLiveError(error) ? error.statusCode : 400); return { message: error instanceof Error ? error.message : "Invalid Tmall live session payload." }; } }); app.delete("/api/platforms/jd/live-session", async (_request, reply) => { jdSessionManager.clearManagedSession("legacy-live-session-clear"); reply.code(204); return null; }); app.delete("/api/platforms/tmall/live-session", async (_request, reply) => { tmallSessionManager.clearManagedSession("legacy-live-session-clear"); 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(); } if (request.params.platform === "tmall") { tmallLiveService.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 = await 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 = await 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 = await 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: { query?: string; commentCount?: string; maxPages?: string; mode?: JdSearchMode; }; }>("/api/platforms/jd/live-keyword-preview", async (request, reply) => { try { const query = request.query.query?.trim(); if (!query) { reply.code(400); return { message: "query is required." }; } const commentCount = request.query.commentCount ? Number.parseInt(request.query.commentCount, 10) : undefined; const maxPages = request.query.maxPages ? Number.parseInt(request.query.maxPages, 10) : undefined; const searchPreview = await jdLiveService.previewSearch(query, request.query.mode); const rankedCandidates = rankJdCandidatesForKeyword(query, searchPreview.candidates); const selectedCandidate = rankedCandidates[0]; if (!selectedCandidate) { reply.code(404); return { message: "JD live search returned no valid item candidates for this keyword. Capture a fresher search session or refine the query." }; } const preview = await jdLiveService.previewProduct(selectedCandidate.skuId, { commentCount, maxPages: Number.isNaN(maxPages ?? Number.NaN) ? undefined : maxPages }); return { preview: { query, search: { source: searchPreview.source, candidateCount: searchPreview.candidateCount, selected: selectedCandidate, alternatives: rankedCandidates.slice(1, 4) }, product: preview } }; } catch (error) { reply.code(isJdLiveError(error) ? error.statusCode : 502); return { message: error instanceof Error ? error.message : "JD live keyword preview failed." }; } }); app.get<{ Querystring: { skuId?: string; commentCount?: string; page?: string; maxPages?: 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 page = request.query.page ? Number.parseInt(request.query.page, 10) : undefined; const maxPages = request.query.maxPages ? Number.parseInt(request.query.maxPages, 10) : undefined; const preview = await jdLiveService.previewReviews(skuId, { commentCount, page: Number.isNaN(page ?? Number.NaN) ? undefined : page, maxPages: Number.isNaN(maxPages ?? Number.NaN) ? undefined : maxPages }); 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<{ Querystring: { skuId?: string; commentCount?: string; page?: string; maxPages?: string; }; }>("/api/platforms/jd/live-product-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 page = request.query.page ? Number.parseInt(request.query.page, 10) : undefined; const maxPages = request.query.maxPages ? Number.parseInt(request.query.maxPages, 10) : undefined; const preview = await jdLiveService.previewProduct(skuId, { commentCount, page: Number.isNaN(page ?? Number.NaN) ? undefined : page, maxPages: Number.isNaN(maxPages ?? Number.NaN) ? undefined : maxPages }); return { preview }; } catch (error) { reply.code(isJdLiveError(error) ? error.statusCode : 502); return { message: error instanceof Error ? error.message : "JD live product preview failed." }; } }); app.get<{ Querystring: { itemId?: string }; }>("/api/platforms/tmall/live-detail-preview", async (request, reply) => { try { const itemId = request.query.itemId?.trim(); if (!itemId) { reply.code(400); return { message: "itemId is required." }; } const preview = await tmallLiveService.previewDetail(itemId); return { preview }; } catch (error) { reply.code(isTmallLiveError(error) ? error.statusCode : 502); return { message: error instanceof Error ? error.message : "Tmall live detail preview failed." }; } }); app.get<{ Querystring: { query?: string }; }>("/api/platforms/tmall/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 tmallLiveService.previewSearch(query); return { preview }; } catch (error) { reply.code(isTmallLiveError(error) ? error.statusCode : 502); return { message: error instanceof Error ? error.message : "Tmall live search preview failed." }; } }); app.get<{ Querystring: { itemId?: string; commentCount?: string; page?: string; maxPages?: string; }; }>("/api/platforms/tmall/live-reviews-preview", async (request, reply) => { try { const itemId = request.query.itemId?.trim(); if (!itemId) { reply.code(400); return { message: "itemId is required." }; } const commentCount = request.query.commentCount ? Number.parseInt(request.query.commentCount, 10) : undefined; const page = request.query.page ? Number.parseInt(request.query.page, 10) : undefined; const maxPages = request.query.maxPages ? Number.parseInt(request.query.maxPages, 10) : undefined; const preview = await tmallLiveService.previewReviews(itemId, { commentCount, page: Number.isNaN(page ?? Number.NaN) ? undefined : page, maxPages: Number.isNaN(maxPages ?? Number.NaN) ? undefined : maxPages }); return { preview }; } catch (error) { reply.code(isTmallLiveError(error) ? error.statusCode : 502); return { message: error instanceof Error ? error.message : "Tmall live reviews preview failed." }; } }); app.get<{ Querystring: { itemId?: string; commentCount?: string; page?: string; maxPages?: string; }; }>("/api/platforms/tmall/live-product-preview", async (request, reply) => { try { const itemId = request.query.itemId?.trim(); if (!itemId) { reply.code(400); return { message: "itemId is required." }; } const commentCount = request.query.commentCount ? Number.parseInt(request.query.commentCount, 10) : undefined; const page = request.query.page ? Number.parseInt(request.query.page, 10) : undefined; const maxPages = request.query.maxPages ? Number.parseInt(request.query.maxPages, 10) : undefined; const preview = await tmallLiveService.previewProduct(itemId, { commentCount, page: Number.isNaN(page ?? Number.NaN) ? undefined : page, maxPages: Number.isNaN(maxPages ?? Number.NaN) ? undefined : maxPages }); return { preview }; } catch (error) { reply.code(isTmallLiveError(error) ? error.statusCode : 502); return { message: error instanceof Error ? error.message : "Tmall live product preview failed." }; } }); app.post<{ Body?: { dryRun?: boolean; asOf?: string; rawRetentionDays?: number; reportRetentionDays?: number; }; }>("/api/retention/cleanup", async (request, reply) => { try { const cleanup = store.runRetentionCleanup(request.body ?? {}); return { cleanup }; } catch (error) { reply.code(400); return { message: error instanceof Error ? error.message : "Invalid retention cleanup request." }; } }); 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.hijack(); reply.raw.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", "Access-Control-Allow-Origin": request.headers.origin ?? "*", Vary: "Origin" }); const writeSnapshot = (nextTask = task) => { reply.raw.write(`event: task.snapshot\n`); reply.raw.write(`data: ${JSON.stringify({ task: nextTask })}\n\n`); }; const unsubscribe = store.subscribeToTask(request.params.taskId, writeSnapshot); const heartbeat = setInterval(() => { reply.raw.write(`: keep-alive\n\n`); }, 15000); writeSnapshot(task); request.raw.on("close", () => { clearInterval(heartbeat); unsubscribe(); }); }); return app; }