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
+14
View File
@@ -0,0 +1,14 @@
{
"name": "@devos/api-client",
"version": "1.0.0",
"private": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json"
},
"dependencies": {
"@devos/types": "workspace:*",
"@devos/auth": "workspace:*"
}
}
+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);
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"types": [
"node"
],
"lib": [
"ES2022",
"DOM"
]
},
"include": [
"src/**/*.ts"
]
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "@devos/auth",
"version": "1.0.0",
"private": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json"
},
"dependencies": {
"@devos/types": "workspace:*"
}
}
+153
View File
@@ -0,0 +1,153 @@
/**
* # @devos/auth
* 认证相关的纯逻辑实现:HS256 JWT 签名/校验、密码哈希/校验。
* 设计原则:零第三方依赖、算法固定(防止 alg 混淆)、常量时间比较。
*/
import { createHmac, randomBytes, randomUUID, timingSafeEqual, scrypt as scryptCb } from "node:crypto";
import { promisify } from "node:util";
/** # scrypt Promise 封装 **/
const scrypt = promisify(scryptCb) as (
password: string,
salt: Buffer,
keylen: number,
) => Promise<Buffer>;
/** # base64url 编码
* @param input 待编码字符串
* @returns base64url 字符串(无填充)
*/
function b64url(input: Buffer | string): string {
return Buffer.from(input).toString("base64url");
}
/** # base64url 解码 */
function b64urlDecode(input: string): Buffer {
return Buffer.from(input, "base64url");
}
/** # JWT 载荷结构 **/
export interface JwtPayload {
/** # 用户 ID **/
sub: string;
/** # 登录邮箱 **/
email: string;
/** # 登录时激活的工作区 ID **/
wid: string | null;
/** # 令牌唯一 ID **/
jti: string;
/** # 签发时间(秒) **/
iat: number;
/** # 过期时间(秒) **/
exp: number;
}
/** # 签发选项 **/
export interface SignOptions {
/** # 有效期(秒),默认 12 小时 **/
expiresInSec?: number;
/** # 当前时间(秒),用于测试注入 **/
nowSec?: number;
}
/** # 签发 HS256 JWT
* @param payload 业务载荷(不含 iat/exp/jti)
* @param secret 应用密钥
* @param options 签发选项
* @returns JWT 字符串
*/
export function signJwt(
payload: Omit<JwtPayload, "iat" | "exp" | "jti">,
secret: string,
options: SignOptions = {},
): string {
const now = options.nowSec ?? Math.floor(Date.now() / 1000);
const expiresInSec = options.expiresInSec ?? 12 * 3600;
const header = { alg: "HS256", typ: "JWT" };
const body: JwtPayload = { ...payload, jti: randomUUID(), iat: now, exp: now + expiresInSec };
const head = b64url(JSON.stringify(header));
const data = b64url(JSON.stringify(body));
const sig = createHmac("sha256", secret).update(`${head}.${data}`).digest("base64url");
return `${head}.${data}.${sig}`;
}
/** # 校验结果 **/
export type VerifyResult = { ok: true; payload: JwtPayload } | { ok: false; reason: string };
/** # 校验 HS256 JWT
* 安全约束:仅接受 alg=HS256 的令牌;签名使用常量时间比较;校验 exp 过期。
* @param token JWT 字符串
* @param secret 应用密钥
* @returns 校验结果
*/
export function verifyJwt(token: string, secret: string, nowSec?: number): VerifyResult {
const parts = token.split(".");
if (parts.length !== 3) return { ok: false, reason: "MALFORMED" };
const [head, data, sig] = parts;
let header: { alg?: string };
try {
header = JSON.parse(b64urlDecode(head).toString("utf8"));
} catch {
return { ok: false, reason: "MALFORMED" };
}
if (header.alg !== "HS256") return { ok: false, reason: "BAD_ALG" };
const expected = createHmac("sha256", secret).update(`${head}.${data}`).digest();
let given: Buffer;
try {
given = b64urlDecode(sig);
} catch {
return { ok: false, reason: "MALFORMED" };
}
if (given.length !== expected.length || !timingSafeEqual(given, expected)) {
return { ok: false, reason: "BAD_SIGNATURE" };
}
let payload: JwtPayload;
try {
payload = JSON.parse(b64urlDecode(data).toString("utf8"));
} catch {
return { ok: false, reason: "MALFORMED" };
}
const now = nowSec ?? Math.floor(Date.now() / 1000);
if (typeof payload.exp !== "number" || payload.exp <= now) return { ok: false, reason: "EXPIRED" };
return { ok: true, payload };
}
/** # 密码哈希
* Bun 运行时下使用 argon2id;否则回退 scrypt。
* @param password 明文密码
* @returns 哈希字符串
*/
export async function hashPassword(password: string): Promise<string> {
const bun = (globalThis as Record<string, unknown>).Bun as typeof Bun | undefined;
if (bun?.password) {
return bun.password.hash(password, { algorithm: "argon2id" });
}
const salt = randomBytes(16);
const key = await scrypt(password, salt, 32);
return `scrypt$${salt.toString("hex")}$${key.toString("hex")}`;
}
/** # 校验密码
* @param password 明文密码
* @param hash 存储的哈希
* @returns 是否匹配
*/
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
if (hash.startsWith("scrypt$")) {
const [, saltHex, keyHex] = hash.split("$");
const key = await scrypt(password, Buffer.from(saltHex, "hex"), 32);
const expected = Buffer.from(keyHex, "hex");
return key.length === expected.length && timingSafeEqual(key, expected);
}
const bun = (globalThis as Record<string, unknown>).Bun as typeof Bun | undefined;
if (bun?.password) {
return bun.password.verify(password, hash);
}
return false;
}
/** # 生成随机令牌(如 API 临时凭证) */
export function randomToken(bytes = 32): string {
return randomBytes(bytes).toString("hex");
}
+15
View File
@@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"types": [
"node",
"bun-types"
]
},
"include": [
"src/**/*.ts"
]
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "@devos/types",
"version": "1.0.0",
"private": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json"
}
}
+276
View File
@@ -0,0 +1,276 @@
/**
* # @devos/types
* 前后端共享的领域类型与常量定义。
* 该包不依赖任何运行时库,仅类型与枚举。
*/
/** # 资源类型枚举
* DevOS 统一资源模型支持的资源类型(V1 子集 + 预留)。
*/
export const RESOURCE_TYPES = [
"SERVER",
"DOMAIN",
"WEBSITE",
"REPOSITORY",
"CERTIFICATE",
"DATABASE",
"CONTAINER",
"EMAIL_ACCOUNT",
"PROJECT",
"TASK",
"NOTE",
"DOCUMENT",
"BACKUP",
"DEPLOYMENT",
"MONITOR",
"CREDENTIAL",
"AUTOMATION",
"AI_RUN",
] as const;
export type ResourceType = (typeof RESOURCE_TYPES)[number];
/** # 资源状态枚举 */
export const RESOURCE_STATUSES = [
"PENDING",
"ACTIVE",
"ERROR",
"OFFLINE",
"ARCHIVED",
"UNKNOWN",
] as const;
export type ResourceStatus = (typeof RESOURCE_STATUSES)[number];
/** # 工作区角色 */
export const WORKSPACE_ROLES = ["OWNER", "ADMIN", "MEMBER", "VIEWER"] as const;
export type WorkspaceRole = (typeof WORKSPACE_ROLES)[number];
/** # 风险等级 */
export const RISK_LEVELS = ["LOW", "MEDIUM", "HIGH", "CRITICAL"] as const;
export type RiskLevel = (typeof RISK_LEVELS)[number];
/** # 审计主体类型 */
export const ACTOR_TYPES = ["USER", "SYSTEM", "AGENT", "MCP"] as const;
export type ActorType = (typeof ACTOR_TYPES)[number];
/** # 任务状态(job 状态机) */
export const JOB_STATUSES = [
"PENDING",
"RUNNING",
"SUCCEEDED",
"FAILED",
"CANCELLED",
"DEAD",
] as const;
export type JobStatus = (typeof JOB_STATUSES)[number];
/** # 部署状态 */
export const DEPLOYMENT_STATUSES = [
"QUEUED",
"RUNNING",
"SUCCEEDED",
"FAILED",
"CANCELLED",
] as const;
export type DeploymentStatus = (typeof DEPLOYMENT_STATUSES)[number];
/** # 触发器类型 */
export const TRIGGER_TYPES = ["MANUAL", "CRON", "EVENT", "WEBHOOK"] as const;
export type TriggerType = (typeof TRIGGER_TYPES)[number];
/** # 规范事件名常量
* 事件命名遵循 `<resource>.<verb>` / `<domain>.<verb>` 约定。
*/
export const EVENTS = {
deploymentCreated: "deployment.created",
deploymentStarted: "deployment.started",
deploymentSuccess: "deployment.success",
deploymentFailed: "deployment.failed",
serverOnline: "server.online",
serverOffline: "server.offline",
websiteDown: "website.down",
websiteRecovered: "website.recovered",
containerRestarted: "container.restarted",
taskCreated: "task.created",
taskCompleted: "task.completed",
} as const;
export type EventName = (typeof EVENTS)[keyof typeof EVENTS] | (string & {});
/** # API 错误码 */
export const ERROR_CODES = {
VALIDATION_ERROR: "VALIDATION_ERROR",
UNAUTHORIZED: "UNAUTHORIZED",
FORBIDDEN: "FORBIDDEN",
RESOURCE_NOT_FOUND: "RESOURCE_NOT_FOUND",
CONFLICT: "CONFLICT",
RATE_LIMITED: "RATE_LIMITED",
INTERNAL_ERROR: "INTERNAL_ERROR",
UPSTREAM_FAILURE: "UPSTREAM_FAILURE",
TIMEOUT: "TIMEOUT",
} as const;
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
/** # API 统一错误结构 */
export interface ApiErrorBody {
error: {
code: ErrorCode | string;
message: string;
requestId: string;
details?: unknown;
};
}
/** # 资源通用视图(API 返回) */
export interface ResourceView {
id: string;
workspaceId: string;
type: ResourceType;
name: string;
provider: string | null;
status: ResourceStatus;
metadata: Record<string, unknown>;
createdAt: string;
updatedAt: string;
}
/** # 服务器视图 */
export interface ServerView {
id: string;
resourceId: string;
workspaceId: string;
name: string;
host: string;
port: number;
username: string;
authType: string;
provider: string;
status: ResourceStatus;
osInfo: string | null;
lastSeenAt: string | null;
createdAt: string;
}
/** # 容器视图 */
export interface ContainerView {
id: string;
containerId: string;
name: string;
image: string;
state: string;
ports: Record<string, unknown> | null;
serverId: string;
}
/** # 网站视图 */
export interface WebsiteView {
id: string;
resourceId: string;
workspaceId: string;
name: string;
primaryDomain: string | null;
serverId: string | null;
repositoryUrl: string | null;
deployType: string;
port: number;
healthPath: string;
status: ResourceStatus;
createdAt: string;
}
/** # 部署视图 */
export interface DeploymentView {
id: string;
resourceId: string;
websiteId: string;
status: DeploymentStatus;
trigger: string;
progress: number;
commit: string | null;
createdAt: string;
finishedAt: string | null;
}
/** # 域名视图 */
export interface DomainView {
id: string;
resourceId: string;
workspaceId: string;
name: string;
provider: string;
status: ResourceStatus;
createdAt: string;
}
/** # 域名记录视图 */
export interface DomainRecordView {
id: string;
type: string;
name: string;
value: string;
ttl: number;
proxied: boolean;
}
/** # 健康检查响应 */
export interface HealthView {
status: "ok";
service: string;
version: string;
time: string;
}
/** # 认证用户视图 */
export interface AuthUserView {
id: string;
email: string;
name: string | null;
activeWorkspaceId: string | null;
workspaces: { id: string; name: string; slug: string; role: WorkspaceRole }[];
}
// ============ Phase 2 常量 ============
/** # 规范关系类型(有向,Phase 2 标准化) **/
export const RELATION_TYPES = {
projectHasRepository: "PROJECT_HAS_REPOSITORY",
projectHasWebsite: "PROJECT_HAS_WEBSITE",
projectHasServer: "PROJECT_HAS_SERVER",
projectHasTask: "PROJECT_HAS_TASK",
projectHasNote: "PROJECT_HAS_NOTE",
projectHasDomain: "PROJECT_HAS_DOMAIN",
websiteDeployedToServer: "WEBSITE_DEPLOYED_TO_SERVER",
websiteUsesDomain: "WEBSITE_USES_DOMAIN",
websiteHasDeployment: "WEBSITE_HAS_DEPLOYMENT",
websiteHasMonitor: "WEBSITE_HAS_MONITOR",
websiteHasCertificate: "WEBSITE_HAS_CERTIFICATE",
deploymentFromRepository: "DEPLOYMENT_FROM_REPOSITORY",
deploymentToServer: "DEPLOYMENT_TO_SERVER",
serverHasContainer: "SERVER_HAS_CONTAINER",
} as const;
export type RelationType = (typeof RELATION_TYPES)[keyof typeof RELATION_TYPES];
/** # 任务状态 **/
export const TASK_STATUSES = ["BACKLOG", "TODO", "DOING", "REVIEW", "DONE"] as const;
export type TaskStatus = (typeof TASK_STATUSES)[number];
/** # 任务优先级 **/
export const TASK_PRIORITIES = ["LOW", "MEDIUM", "HIGH", "URGENT"] as const;
export type TaskPriority = (typeof TASK_PRIORITIES)[number];
/** # 部署环境 **/
export const ENVIRONMENTS = ["development", "staging", "production"] as const;
export type DeployEnvironment = (typeof ENVIRONMENTS)[number];
/** # Git 提供商 **/
export const GIT_PROVIDERS = ["GITHUB", "GITLAB", "GITEA"] as const;
export type GitProvider = (typeof GIT_PROVIDERS)[number];
/** # 新增动作类型(自动化 Phase F) **/
export const AUTOMATION_ACTION_TYPES = [
"LOG",
"WEBHOOK",
"DEPLOY_WEBSITE",
"CONTAINER_RESTART",
"TASK_CREATE",
"NOTE_CREATE",
"NOTIFICATION_SEND",
] as const;
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"declaration": true
},
"include": ["src/**/*.ts"]
}
+14
View File
@@ -0,0 +1,14 @@
{
"name": "@devos/ui",
"version": "1.0.0",
"private": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json"
},
"dependencies": {
"@devos/types": "workspace:*",
"antd": "^5.22.0"
}
}