41874ab391
新增分支与指定提交的预览部署 SPA 和 Jenkins 代理服务 新增多实例 Docker 预览流水线、端口租约、实时健康状态和卸载能力 新增 /build 内网路由、systemd 部署资产和运维文档 修正容器 Nginx 健康检查探针
129 lines
3.1 KiB
TypeScript
129 lines
3.1 KiB
TypeScript
import type { CreateDeploymentInput, PreviewDeployment } from './types';
|
||
|
||
const API_BASE = '/api/preview-deployer';
|
||
|
||
export class PreviewDeployerApiError extends Error {
|
||
status: number;
|
||
|
||
constructor(message: string, status: number) {
|
||
super(message);
|
||
this.name = 'PreviewDeployerApiError';
|
||
this.status = status;
|
||
}
|
||
}
|
||
|
||
export interface PreviewDeployerSession {
|
||
authenticated: boolean;
|
||
}
|
||
|
||
export async function getSession(signal?: AbortSignal) {
|
||
const session = await request<PreviewDeployerSession | null>('/session', {
|
||
signal,
|
||
});
|
||
return session ?? { authenticated: true };
|
||
}
|
||
|
||
export function createSession(accessToken: string) {
|
||
return request<PreviewDeployerSession>('/session', {
|
||
method: 'POST',
|
||
body: { accessToken },
|
||
});
|
||
}
|
||
|
||
export function deleteSession() {
|
||
return request<void>('/session', { method: 'DELETE' });
|
||
}
|
||
|
||
export async function listDeployments(signal?: AbortSignal) {
|
||
const payload = await request<
|
||
PreviewDeployment[] | { deployments: PreviewDeployment[] }
|
||
>('/deployments', { signal });
|
||
return Array.isArray(payload) ? payload : payload.deployments;
|
||
}
|
||
|
||
export function createDeployment(input: CreateDeploymentInput) {
|
||
return request<PreviewDeployment>('/deployments', {
|
||
method: 'POST',
|
||
body: input,
|
||
});
|
||
}
|
||
|
||
export function uninstallDeployment(deploymentId: string) {
|
||
return request<PreviewDeployment>(
|
||
`/deployments/${encodeURIComponent(deploymentId)}/uninstall`,
|
||
{ method: 'POST', body: {} },
|
||
);
|
||
}
|
||
|
||
interface RequestOptions {
|
||
method?: string;
|
||
body?: unknown;
|
||
signal?: AbortSignal;
|
||
}
|
||
|
||
async function request<T>(
|
||
path: string,
|
||
options: RequestOptions = {},
|
||
): Promise<T> {
|
||
const headers: Record<string, string> = { Accept: 'application/json' };
|
||
const init: RequestInit = {
|
||
method: options.method ?? 'GET',
|
||
headers,
|
||
signal: options.signal,
|
||
credentials: 'same-origin',
|
||
};
|
||
if (options.body !== undefined) {
|
||
headers['Content-Type'] = 'application/json';
|
||
init.body = JSON.stringify(options.body);
|
||
}
|
||
|
||
const response = await fetch(`${API_BASE}${path}`, init);
|
||
const responseText = await response.text();
|
||
const payload = parseJson(responseText);
|
||
if (!response.ok) {
|
||
throw new PreviewDeployerApiError(
|
||
readErrorMessage(payload) || `请求失败(HTTP ${response.status})`,
|
||
response.status,
|
||
);
|
||
}
|
||
return unwrapPayload<T>(payload);
|
||
}
|
||
|
||
function parseJson(value: string): unknown {
|
||
if (!value.trim()) {
|
||
return null;
|
||
}
|
||
try {
|
||
return JSON.parse(value) as unknown;
|
||
} catch {
|
||
return value;
|
||
}
|
||
}
|
||
|
||
function unwrapPayload<T>(payload: unknown): T {
|
||
if (isRecord(payload) && 'data' in payload) {
|
||
return payload.data as T;
|
||
}
|
||
return payload as T;
|
||
}
|
||
|
||
function readErrorMessage(payload: unknown) {
|
||
if (typeof payload === 'string') {
|
||
return payload.trim();
|
||
}
|
||
if (!isRecord(payload)) {
|
||
return '';
|
||
}
|
||
if (typeof payload.message === 'string') {
|
||
return payload.message;
|
||
}
|
||
if (isRecord(payload.error) && typeof payload.error.message === 'string') {
|
||
return payload.error.message;
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||
return typeof value === 'object' && value !== null;
|
||
}
|