312 lines
8.5 KiB
TypeScript
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;
|
|
}
|