312 lines
8.5 KiB
TypeScript

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;
}