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.

716 lines
18 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,
MothershipAddressCandidate,
MothershipCandidatesResult,
PaginatedResult,
QueueStats,
QuoteQueryLogRecord,
QuoteDetail,
QuoteRequestBody,
Priority1QuoteRequestBody,
FlockQuoteRequestBody,
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 hostCreateFlockQuote(
apiBaseUrl: string,
serviceToken: string | undefined,
body: FlockQuoteRequestBody,
): Promise<ApiResponse<CreateQuoteResult>> {
const response = await fetch(
`${apiBaseUrl}/api/flock/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 type MothershipSuggestResult = {
candidates: MothershipAddressCandidate[];
};
/** 登录后一级:单边键入 MotherShip 地址联想 */
export async function hostSuggestMothershipAddress(
apiBaseUrl: string,
serviceToken: string | undefined,
customerId: string,
query: string,
): Promise<ApiResponse<MothershipSuggestResult>> {
const controller = new AbortController();
const timeout = setTimeout(
() => controller.abort(),
CANDIDATES_FETCH_TIMEOUT_MS,
);
try {
const response = await fetch(
`${apiBaseUrl}/api/addresses/mothership-suggest`,
hostFetchInit(serviceToken, customerId, {
method: "POST",
body: JSON.stringify({
customer_id: customerId,
query,
}),
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 EmbedDemoLoginType = "password" | "api_key";
export type EmbedDemoSession = {
customer_id: string;
/** 登录类型:账号密码 / API Key旧会话缺省为 password */
login_type?: EmbedDemoLoginType;
markup: MarkupConfig;
/** 客户是否已绑定承运商官网账密(驱动登录后查价 UI */
providers?: {
mothership: { has_password: boolean };
flock: { has_password: boolean };
};
};
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({
login_type: "password",
customer_id: customerId,
password,
}),
cache: "no-store",
});
return parseJson(response);
}
/** API Key 登录:只填 Key服务端反查租户 */
export async function embedDemoLoginWithKey(
apiBaseUrl: string,
apiKey: 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({ login_type: "api_key", api_key: apiKey }),
cache: "no-store",
});
return parseJson(response);
}
export type EmbedProviderCredential = {
provider: "mothership" | "flock";
email: string | null;
/** 明文密码embed 自助核对用;无配置时为 null */
password: string | null;
has_password: boolean;
updated_at: string | null;
};
/** 读取当前演示客户各承运商账密状态(邮箱 + 是否已配置) */
export async function embedGetProviderCredentials(
apiBaseUrl: string,
): Promise<ApiResponse<{ customer_id: string; list: EmbedProviderCredential[] }>> {
const response = await fetch(
`${apiBaseUrl}/api/embed-demo/provider-credentials`,
{ credentials: "include", cache: "no-store" },
);
return parseJson(response);
}
/** 保存/修改当前演示客户承运商账密 */
export async function embedSaveProviderCredential(
apiBaseUrl: string,
provider: "mothership" | "flock",
email: string,
password: string,
): Promise<ApiResponse<EmbedProviderCredential>> {
const response = await fetch(
`${apiBaseUrl}/api/embed-demo/provider-credentials`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ provider, email, 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 type ProviderCredentialProvider = "mothership" | "flock";
export type ProviderCredentialRecord = {
provider: ProviderCredentialProvider;
email: string | null;
/** 管理端查看接口回传明文密码 */
password?: string | null;
has_password: boolean;
updated_at: string | null;
};
export async function adminGetProviderCredentials(
apiBaseUrl: string,
token: string,
customerId: string,
): Promise<
ApiResponse<{ customer_id: string; list: ProviderCredentialRecord[] }>
> {
const response = await fetch(
`${apiBaseUrl}/api/admin/customers/${customerId}/provider-credentials`,
{
headers: adminHeaders(token),
cache: "no-store",
},
);
return parseJson(response);
}
export async function adminUpsertProviderCredential(
apiBaseUrl: string,
token: string,
customerId: string,
body: {
provider: ProviderCredentialProvider;
email?: string;
password?: string;
clear?: boolean;
},
): Promise<ApiResponse<ProviderCredentialRecord>> {
const response = await fetch(
`${apiBaseUrl}/api/admin/customers/${customerId}/provider-credentials`,
{
method: "PUT",
headers: adminHeaders(token),
body: JSON.stringify(body),
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);
}