You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

536 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import type {
AddressInput,
AlertRecord,
ApiResponse,
CreateQuoteResult,
CreateHostCustomerResponse,
HostCustomerRecord,
LoginResponse,
MarkupConfig,
MetricsDashboard,
MothershipCandidatesResult,
PaginatedResult,
QueueStats,
QuoteQueryLogRecord,
QuoteDetail,
QuoteRequestBody,
Priority1QuoteRequestBody,
RpaStatus,
} from "@/lib/frontend/types";
import { CANDIDATES_FETCH_TIMEOUT_MS } from "@/lib/frontend/constants";
async function parseJson<T>(response: Response): Promise<ApiResponse<T>> {
const contentType = response.headers.get("content-type") ?? "";
if (!contentType.includes("application/json")) {
const hint =
response.status === 404
? "询价结果接口不可用GET /api/quotes/{id}),请重新部署并确认已包含动态路由"
: response.ok
? "响应解析失败"
: `服务异常 (HTTP ${response.status})`;
return {
code: "INTERNAL_ERROR",
message: hint,
data: null,
};
}
try {
return (await response.json()) as ApiResponse<T>;
} catch {
return {
code: "INTERNAL_ERROR",
message: "响应解析失败",
data: null,
};
}
}
function hostHeaders(
serviceToken: string | undefined,
customerId: string,
): Record<string, string> {
const headers: Record<string, string> = {
"X-Customer-Id": customerId,
"Content-Type": "application/json",
"Cache-Control": "no-store",
};
if (serviceToken?.trim()) {
headers.Authorization = `Bearer ${serviceToken}`;
}
return headers;
}
function hostFetchInit(
serviceToken: string | undefined,
customerId: string,
init: RequestInit,
): RequestInit {
return {
...init,
headers: hostHeaders(serviceToken, customerId),
credentials: "include",
cache: "no-store",
};
}
function adminHeaders(token: string): Record<string, string> {
return {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"Cache-Control": "no-store",
};
}
export async function hostCreateQuote(
apiBaseUrl: string,
serviceToken: string | undefined,
body: QuoteRequestBody,
): Promise<ApiResponse<CreateQuoteResult>> {
const response = await fetch(
`${apiBaseUrl}/api/quotes`,
hostFetchInit(serviceToken, body.customer_id, {
method: "POST",
body: JSON.stringify(body),
}),
);
return parseJson(response);
}
export async function hostCreatePriority1Quote(
apiBaseUrl: string,
serviceToken: string | undefined,
body: Priority1QuoteRequestBody,
): Promise<ApiResponse<CreateQuoteResult>> {
const response = await fetch(
`${apiBaseUrl}/api/priority1/quotes`,
hostFetchInit(serviceToken, body.customer_id, {
method: "POST",
body: JSON.stringify(body),
}),
);
return parseJson(response);
}
export async function hostFetchMothershipCandidates(
apiBaseUrl: string,
serviceToken: string | undefined,
customerId: string,
pickup: Pick<AddressInput, "street" | "city" | "state" | "zip">,
delivery: Pick<AddressInput, "street" | "city" | "state" | "zip">,
): Promise<ApiResponse<MothershipCandidatesResult>> {
const controller = new AbortController();
const timeout = setTimeout(
() => controller.abort(),
CANDIDATES_FETCH_TIMEOUT_MS,
);
try {
const response = await fetch(
`${apiBaseUrl}/api/addresses/mothership-candidates`,
hostFetchInit(serviceToken, customerId, {
method: "POST",
body: JSON.stringify({
customer_id: customerId,
pickup_address: pickup,
delivery_address: delivery,
}),
signal: controller.signal,
}),
);
return parseJson(response);
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
return {
code: "QUOTE_TIMEOUT",
message: "地址联想超时MotherShip 响应较慢,请稍后重试",
data: null,
};
}
throw error;
} finally {
clearTimeout(timeout);
}
}
export async function hostPreheatQuoteSession(
apiBaseUrl: string,
serviceToken: string | undefined,
customerId: string,
quoteSessionId: string,
): Promise<void> {
try {
await fetch(
`${apiBaseUrl}/api/addresses/mothership-candidates/preheat`,
hostFetchInit(serviceToken, customerId, {
method: "POST",
body: JSON.stringify({
customer_id: customerId,
quote_session_id: quoteSessionId,
}),
}),
);
} catch {
/* 预热失败不阻断主流程 */
}
}
export async function hostConfirmMothershipAddresses(
apiBaseUrl: string,
serviceToken: string | undefined,
customerId: string,
quoteSessionId: string,
pickup: MothershipCandidatesResult["pickup_candidates"][number],
delivery: MothershipCandidatesResult["delivery_candidates"][number],
): Promise<ApiResponse<{ confirmed: boolean }>> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 300_000);
try {
const response = await fetch(
`${apiBaseUrl}/api/addresses/mothership-candidates/confirm`,
hostFetchInit(serviceToken, customerId, {
method: "POST",
body: JSON.stringify({
customer_id: customerId,
quote_session_id: quoteSessionId,
pickup,
delivery,
}),
signal: controller.signal,
}),
);
return parseJson(response);
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
return {
code: "QUOTE_TIMEOUT",
message: "地址确认超时,请重试",
data: null,
};
}
throw error;
} finally {
clearTimeout(timeout);
}
}
export async function hostReleaseQuoteSession(
apiBaseUrl: string,
serviceToken: string | undefined,
customerId: string,
quoteSessionId: string,
): Promise<void> {
try {
await fetch(
`${apiBaseUrl}/api/addresses/mothership-candidates/release`,
hostFetchInit(serviceToken, customerId, {
method: "POST",
body: JSON.stringify({
customer_id: customerId,
quote_session_id: quoteSessionId,
}),
}),
);
} catch {
/* 释放失败不阻断 UI */
}
}
export async function hostGetQuote(
apiBaseUrl: string,
serviceToken: string | undefined,
customerId: string,
quoteId: string,
): Promise<ApiResponse<QuoteDetail>> {
const response = await fetch(
`${apiBaseUrl}/api/quotes/${quoteId}`,
hostFetchInit(serviceToken, customerId, {
method: "GET",
}),
);
return parseJson(response);
}
export type EmbedDemoSession = {
customer_id: string;
markup: MarkupConfig;
};
export async function embedDemoLogin(
apiBaseUrl: string,
customerId: string,
password: string,
): Promise<ApiResponse<EmbedDemoSession>> {
const response = await fetch(`${apiBaseUrl}/api/embed-demo/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ customer_id: customerId, password }),
cache: "no-store",
});
return parseJson(response);
}
export async function embedDemoMe(
apiBaseUrl: string,
): Promise<ApiResponse<EmbedDemoSession>> {
const response = await fetch(`${apiBaseUrl}/api/embed-demo/me`, {
credentials: "include",
cache: "no-store",
});
return parseJson(response);
}
export async function embedDemoLogout(apiBaseUrl: string): Promise<void> {
await fetch(`${apiBaseUrl}/api/embed-demo/logout`, {
method: "POST",
credentials: "include",
cache: "no-store",
});
}
export async function adminLogin(
apiBaseUrl: string,
username: string,
password: string,
): Promise<ApiResponse<LoginResponse>> {
const response = await fetch(`${apiBaseUrl}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
cache: "no-store",
});
return parseJson(response);
}
export async function adminGetAlerts(
apiBaseUrl: string,
token: string,
page: number,
size: number,
type?: string,
status?: string,
): Promise<ApiResponse<PaginatedResult<AlertRecord>>> {
const params = new URLSearchParams({
page: String(page),
size: String(size),
});
if (type) params.set("type", type);
if (status) params.set("status", status);
const response = await fetch(`${apiBaseUrl}/api/alerts?${params}`, {
headers: adminHeaders(token),
cache: "no-store",
});
return parseJson(response);
}
export async function adminResolveAlert(
apiBaseUrl: string,
token: string,
alertId: string,
resolverId: string,
): Promise<ApiResponse<AlertRecord>> {
const response = await fetch(`${apiBaseUrl}/api/alerts/${alertId}/resolve`, {
method: "POST",
headers: adminHeaders(token),
body: JSON.stringify({ resolver_id: resolverId }),
cache: "no-store",
});
return parseJson(response);
}
export async function adminGetCustomers(
apiBaseUrl: string,
token: string,
page: number,
size: number,
keyword?: string,
): Promise<ApiResponse<PaginatedResult<HostCustomerRecord>>> {
const params = new URLSearchParams({
page: String(page),
size: String(size),
});
if (keyword) params.set("keyword", keyword);
const response = await fetch(
`${apiBaseUrl}/api/admin/customers?${params}`,
{
headers: adminHeaders(token),
cache: "no-store",
},
);
return parseJson(response);
}
export async function adminCreateCustomer(
apiBaseUrl: string,
token: string,
body: { name: string; remark?: string; embed_password?: string },
): Promise<ApiResponse<CreateHostCustomerResponse>> {
const response = await fetch(`${apiBaseUrl}/api/admin/customers`, {
method: "POST",
headers: adminHeaders(token),
body: JSON.stringify(body),
cache: "no-store",
});
return parseJson(response);
}
export async function adminUpdateCustomer(
apiBaseUrl: string,
token: string,
customerId: string,
body: {
name?: string;
status?: "active" | "disabled";
remark?: string | null;
embed_password?: string;
},
): Promise<ApiResponse<HostCustomerRecord>> {
const response = await fetch(
`${apiBaseUrl}/api/admin/customers/${customerId}`,
{
method: "PATCH",
headers: adminHeaders(token),
body: JSON.stringify(body),
cache: "no-store",
},
);
return parseJson(response);
}
export async function adminRotateCustomerKey(
apiBaseUrl: string,
token: string,
customerId: string,
): Promise<ApiResponse<CreateHostCustomerResponse>> {
const response = await fetch(
`${apiBaseUrl}/api/admin/customers/${customerId}/rotate-key`,
{
method: "POST",
headers: adminHeaders(token),
cache: "no-store",
},
);
return parseJson(response);
}
export async function adminGetMarkupConfigs(
apiBaseUrl: string,
token: string,
page: number,
size: number,
keyword?: string,
): Promise<ApiResponse<PaginatedResult<MarkupConfig>>> {
const params = new URLSearchParams({
page: String(page),
size: String(size),
});
if (keyword) params.set("keyword", keyword);
const response = await fetch(
`${apiBaseUrl}/api/admin/markup-configs?${params}`,
{
headers: adminHeaders(token),
cache: "no-store",
},
);
return parseJson(response);
}
export async function adminUpdateMarkupConfig(
apiBaseUrl: string,
token: string,
customerId: string,
body: {
markup_type: "percent" | "fixed";
markup_percent?: number;
markup_fixed_amount?: number;
remark?: string;
},
): Promise<ApiResponse<MarkupConfig>> {
const response = await fetch(
`${apiBaseUrl}/api/admin/markup-configs/${customerId}`,
{
method: "PUT",
headers: adminHeaders(token),
body: JSON.stringify(body),
cache: "no-store",
},
);
return parseJson(response);
}
export async function adminGetMetrics(
apiBaseUrl: string,
token: string,
): Promise<ApiResponse<MetricsDashboard>> {
const response = await fetch(`${apiBaseUrl}/api/metrics/dashboard`, {
headers: adminHeaders(token),
cache: "no-store",
});
return parseJson(response);
}
export async function adminGetRpaStatus(
apiBaseUrl: string,
token: string,
): Promise<ApiResponse<RpaStatus>> {
const response = await fetch(`${apiBaseUrl}/api/rpa/status`, {
headers: adminHeaders(token),
cache: "no-store",
});
return parseJson(response);
}
export async function adminGetQueryLogs(
apiBaseUrl: string,
token: string,
page: number,
size: number,
customerId?: string,
outcome?: string,
): Promise<ApiResponse<PaginatedResult<QuoteQueryLogRecord>>> {
const params = new URLSearchParams({
page: String(page),
size: String(size),
});
if (customerId) params.set("customer_id", customerId);
if (outcome) params.set("outcome", outcome);
const response = await fetch(`${apiBaseUrl}/api/admin/query-logs?${params}`, {
headers: adminHeaders(token),
cache: "no-store",
});
return parseJson(response);
}
export async function adminGetQueueStats(
apiBaseUrl: string,
token: string,
): Promise<ApiResponse<QueueStats>> {
const response = await fetch(`${apiBaseUrl}/api/admin/queues/stats`, {
headers: adminHeaders(token),
cache: "no-store",
});
return parseJson(response);
}
export async function adminPauseWorker(
apiBaseUrl: string,
token: string,
workerId: string,
durationMinutes: number,
): Promise<ApiResponse<{ worker_id: string; message: string }>> {
const response = await fetch(`${apiBaseUrl}/api/rpa/worker/pause`, {
method: "POST",
headers: adminHeaders(token),
body: JSON.stringify({
worker_id: workerId,
duration_minutes: durationMinutes,
}),
cache: "no-store",
});
return parseJson(response);
}