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