930 lines
27 KiB
TypeScript
930 lines
27 KiB
TypeScript
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;
|
|
}
|