DevOS v1 + Phase 2: unified resource model, infra ops, website deployment, AI chat ops

- Monorepo: NestJS API (Fastify) + React web (antd/ProComponents) + shared packages
- PostgreSQL/Prisma (unified resources, relations graph, audit, events, jobs)
- Auth/RBAC/workspace isolation, credential vault (AES-256-GCM)
- Servers/SSH, Docker discovery & lifecycle, Job Center
- Websites & deployments (git provider credential clone, env, health verification)
- Domains/DNS (Cloudflare adapter), certificates (TLS probe), monitoring
- Automation (cron/event/webhook HMAC + notifications), AI Gateway, MCP (29 tools)
- AI chat window (LLM function-calling + rules fallback, SSE streaming, approvals)
- Notes: Vditor WYSIWYG markdown editor, non-blocking autosave
- Tests: 35 unit/security tests; UI audit clean (desktop+mobile, deep links)
This commit is contained in:
phptits
2026-09-10 06:35:55 +00:00
commit 0d6cd37579
209 changed files with 22783 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
/**
* # @devos/api-client
* 浏览器端 API 客户端:统一的 fetch 封装,自动附带 JWT 与工作区头,
* 归一化错误结构(与后端 error contract 对齐)。
*/
import type { ApiErrorBody } from "@devos/types";
/** # 客户端配置 **/
export interface ApiClientOptions {
/** # API 基础地址,默认同源 /api/v1 **/
baseUrl?: string;
/** # 令牌提供器(返回当前 JWT 或 null) **/
getToken: () => string | null;
/** # 工作区 ID 提供器(返回当前工作区或 null) **/
getWorkspaceId?: () => string | null;
/** # 401 时的回调(例如跳转登录) **/
onUnauthorized?: () => void;
}
/** # API 错误(客户端侧归一化) **/
export class ApiError extends Error {
/** # 错误码 **/
code: string;
/** # HTTP 状态码 **/
status: number;
/** # 请求 ID **/
requestId: string;
constructor(status: number, code: string, message: string, requestId: string) {
super(message);
this.status = status;
this.code = code;
this.requestId = requestId;
}
}
/** # API 客户端 **/
export class ApiClient {
/** # 基础地址 **/
private baseUrl: string;
private getToken: () => string | null;
private getWorkspaceId?: () => string | null;
private onUnauthorized?: () => void;
constructor(options: ApiClientOptions) {
this.baseUrl = (options.baseUrl ?? "/api/v1").replace(/\/$/, "");
this.getToken = options.getToken;
this.getWorkspaceId = options.getWorkspaceId;
this.onUnauthorized = options.onUnauthorized;
}
/** # 发起请求
* @param method HTTP 方法
* @param path 相对路径(不含 /api/v1)
* @param body 请求体(自动 JSON 序列化)
* @param query 查询参数
* @returns 解析后的 JSON 响应
*/
async request<T>(
method: string,
path: string,
body?: unknown,
query?: Record<string, string | number | boolean | undefined | null>,
): Promise<T> {
const url = new URL(this.baseUrl + path, window.location.origin);
if (query) {
for (const [k, v] of Object.entries(query)) {
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
}
}
const headers: Record<string, string> = {};
const token = this.getToken();
if (token) headers["Authorization"] = `Bearer ${token}`;
const wid = this.getWorkspaceId?.();
if (wid) headers["X-Workspace-Id"] = wid;
if (body !== undefined) headers["Content-Type"] = "application/json";
const res = await fetch(url.toString(), {
method,
headers,
body: body !== undefined ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
let code = "INTERNAL_ERROR";
let message = res.statusText;
let requestId = "";
let parsed: ApiErrorBody | null = null;
try {
parsed = (await res.json()) as ApiErrorBody;
} catch {
/* 非 JSON 错误体 */
}
if (parsed?.error) {
code = parsed.error.code;
message = parsed.error.message;
requestId = parsed.error.requestId;
}
if (res.status === 401) this.onUnauthorized?.();
throw new ApiError(res.status, code, message, requestId);
}
if (res.status === 204) return undefined as T;
return (await res.json()) as T;
}
get<T>(path: string, query?: Record<string, string | number | boolean | undefined | null>) {
return this.request<T>("GET", path, undefined, query);
}
post<T>(path: string, body?: unknown) {
return this.request<T>("POST", path, body);
}
patch<T>(path: string, body?: unknown) {
return this.request<T>("PATCH", path, body);
}
delete<T>(path: string) {
return this.request<T>("DELETE", path);
}
}