v2 platform foundation (docs 01/02/03): Action Runtime + Registry + unified API

- v2 foundation: ActionResult/DevError/ActorContext/ResourceRef/Execution types
- ActionRegistry + ResourceRegistry (namespaced types, FAIL FAST on duplicates)
- ActionRuntime pipeline: validate -> authorize -> middleware -> handler ->
  output validation -> audit -> event -> execution persisted (devos_action_executions)
- Legacy bridge: v1 services registered as devops.*/core.* resources & actions (47 actions, 9 resources)
- Unified Action API: POST /api/v2/actions/:actionId, GET discovery (actions/resources)
- 10 v2 unit tests (registry fail-fast, validation, authz, failure audit, events)
This commit is contained in:
phptits
2026-09-10 21:09:44 +00:00
parent 0d6cd37579
commit b1f17cf637
30 changed files with 60110 additions and 3 deletions
+2
View File
@@ -40,6 +40,7 @@ import { NoteModule } from "./modules/note/note.module";
import { CertificateModule } from "./modules/certificate/certificate.module";
import { GitModule } from "./modules/git/git.module";
import { SearchModule } from "./modules/search/search.module";
import { V2Module } from "./v2/v2.module";
@Module({
imports: [
@@ -71,6 +72,7 @@ import { SearchModule } from "./modules/search/search.module";
CertificateModule,
GitModule,
SearchModule,
V2Module,
],
controllers: [SystemController],
providers: [
+460
View File
@@ -0,0 +1,460 @@
/**
* # v2 遗留功能桥接(docs 03 §44/§103)
* 把 v1 模块服务注册为 v2 Resource/Action(devops.* 命名空间)。
* 这是"六入口一实现"的第一步:REST/MCP/Agent 逐步迁移到 Action Runtime。
* 业务实现完全复用 v1 service(含审计/事件),Action 层只做契约与编排。
*/
import { Injectable, OnModuleInit } from "@nestjs/common";
import type { ActionContext } from "../foundation/types";
import { z } from "zod";
import { actionRegistry, defineAction, type ActionDefinition } from "../registry/action-registry";
import { resourceRegistry } from "../registry/resource-registry";
import { ServerService } from "../../modules/server/server.service";
import { WebsiteService } from "../../modules/website/website.service";
import { DeploymentService } from "../../modules/deployment/deployment.service";
import { TaskService } from "../../modules/task/task.service";
import { NoteService } from "../../modules/note/note.service";
import { DomainService } from "../../modules/domain/domain.service";
import { ContainerService } from "../../modules/container/container.service";
import { MonitoringService } from "../../modules/monitoring/monitoring.service";
import { AutomationService } from "../../modules/automation/automation.service";
import { CredentialService } from "../../modules/credential/credential.service";
import { JobQueueService } from "../../modules/job/job-queue.service";
/** # 注册上下文:workspace 感知的动作执行器包装 **/
type WorkspaceAction<TInput, TOutput> = {
id: string;
resource: string;
name: string;
description: string;
mode?: "sync" | "async";
permission?: string;
dangerous?: boolean;
visibility?: "public" | "internal" | "agent" | "admin";
inputSchema?: z.ZodType<TInput>;
outputSchema?: z.ZodType<TOutput>;
/** # 业务执行:ctx.workspaceId 已由 runtime 注入 actor/organization **/
run: (workspaceId: string, input: TInput) => Promise<TOutput>;
};
@Injectable()
export class LegacyBridgeService implements OnModuleInit {
constructor(
private readonly servers: ServerService,
private readonly websites: WebsiteService,
private readonly deployments: DeploymentService,
private readonly tasks: TaskService,
private readonly notes: NoteService,
private readonly domains: DomainService,
private readonly containers: ContainerService,
private readonly monitoring: MonitoringService,
private readonly automations: AutomationService,
private readonly credentials: CredentialService,
private readonly jobs: JobQueueService,
) {}
/** # 模块初始化:注册全部 devops.* 资源与动作 */
onModuleInit(): void {
this.registerResources();
this.registerServerActions();
this.registerWebsiteActions();
this.registerDeploymentActions();
this.registerTaskActions();
this.registerNoteActions();
this.registerDomainActions();
this.registerContainerActions();
this.registerMonitorActions();
this.registerAutomationActions();
this.registerJobActions();
}
/** # 注册资源类型(schema 元数据) */
private registerResources(): void {
resourceRegistry.register({
type: "devops.server",
label: "Server",
description: "Managed compute target reachable via SSH or local docker socket",
actionIds: ["devops.server.list", "devops.server.get", "devops.server.create", "devops.server.delete", "devops.server.connect", "devops.server.exec"],
ownership: { organization: true },
ui: { icon: "CloudServerOutlined", listColumns: ["name", "host", "status"] },
});
resourceRegistry.register({
type: "devops.website",
label: "Website",
description: "Deployable application bound to server/repository/domain",
actionIds: ["devops.website.list", "devops.website.get", "devops.website.create", "devops.website.update", "devops.website.delete", "devops.website.deploy", "devops.website.health", "devops.website.logs"],
ownership: { organization: true },
ui: { icon: "GlobalOutlined", listColumns: ["name", "primaryDomain", "status"] },
});
resourceRegistry.register({
type: "devops.deployment",
label: "Deployment",
actionIds: ["devops.deployment.list", "devops.deployment.get", "devops.deployment.create", "devops.deployment.cancel"],
ownership: { organization: true },
ui: { icon: "ThunderboltOutlined", listColumns: ["id", "status", "progress"] },
});
resourceRegistry.register({
type: "core.task",
label: "Task",
actionIds: ["core.task.list", "core.task.get", "core.task.create", "core.task.update", "core.task.delete"],
ownership: { organization: true },
ui: { icon: "CheckSquareOutlined", listColumns: ["title", "status", "priority"] },
});
resourceRegistry.register({
type: "core.note",
label: "Note",
actionIds: ["core.note.list", "core.note.get", "core.note.create", "core.note.update", "core.note.delete"],
ownership: { organization: true },
ui: { icon: "ReadOutlined", listColumns: ["title", "tags", "updatedAt"] },
});
resourceRegistry.register({
type: "devops.domain",
label: "Domain",
actionIds: ["devops.domain.list", "devops.domain.get", "devops.domain.create", "devops.domain.delete", "devops.domain.records", "devops.domain.upsertRecord"],
ownership: { organization: true },
ui: { icon: "HddOutlined", listColumns: ["name", "provider", "status"] },
});
resourceRegistry.register({
type: "devops.container",
label: "Container",
actionIds: ["devops.container.list", "devops.container.logs", "devops.container.stats", "devops.container.restart"],
ownership: { organization: true },
ui: { icon: "DeploymentUnitOutlined", listColumns: ["name", "image", "state"] },
});
resourceRegistry.register({
type: "devops.monitor",
label: "Monitor",
actionIds: ["devops.monitor.list", "devops.monitor.create", "devops.monitor.delete"],
ownership: { organization: true },
ui: { icon: "AlertOutlined", listColumns: ["name", "target", "lastStatus"] },
});
resourceRegistry.register({
type: "core.credential",
label: "Credential",
actionIds: ["core.credential.list", "core.credential.create", "core.credential.delete"],
ownership: { organization: true },
ui: { icon: "KeyOutlined", listColumns: ["name", "type", "provider"] },
});
}
/** # 注册单个动作的通用包装 */
private register<TInput, TOutput>(def: WorkspaceAction<TInput, TOutput>): void {
actionRegistry.register(
{
...def,
execute: async (ctx: ActionContext, input: unknown) =>
def.run(ctx.organizationId ?? "", input as TInput),
} as unknown as ActionDefinition<never, never>,
"legacy-bridge",
);
}
/** # devops.server.* */
private registerServerActions(): void {
const Ids = z.object({ id: z.string().min(1) });
this.register({
id: "devops.server.list", resource: "devops.server", name: "List servers",
description: "List all servers in the workspace", inputSchema: z.object({}),
run: (wid) => this.servers.list(wid),
});
this.register({
id: "devops.server.get", resource: "devops.server", name: "Get server",
description: "Get a server by id", inputSchema: Ids,
run: (wid, i) => this.servers.get(wid, i.id),
});
this.register({
id: "devops.server.create", resource: "devops.server", name: "Create server",
description: "Register a new server (SSH or local docker)",
inputSchema: z.object({ name: z.string(), host: z.string(), port: z.number().optional(), username: z.string(), provider: z.enum(["ssh", "local"]).optional(), authType: z.enum(["PASSWORD", "SSH_KEY"]).optional(), password: z.string().optional(), privateKey: z.string().optional(), credentialId: z.string().optional() }),
run: (wid, i) => this.servers.create(wid, i as never),
});
this.register({
id: "devops.server.delete", resource: "devops.server", name: "Delete server",
description: "Delete a server", dangerous: true, inputSchema: Ids,
run: (wid, i) => this.servers.delete(wid, i.id),
});
this.register({
id: "devops.server.connect", resource: "devops.server", name: "Connect check",
description: "Run connectivity check against a server", inputSchema: Ids,
run: (wid, i) => this.servers.connect(wid, i.id),
});
this.register({
id: "devops.server.exec", resource: "devops.server", name: "Execute command",
description: "Execute a shell command on a server via SSH", dangerous: true,
inputSchema: z.object({ id: z.string(), command: z.string().min(1).max(8192), timeoutMs: z.number().optional() }),
run: (wid, i) => this.servers.exec(wid, i.id, { command: i.command, timeoutMs: i.timeoutMs ?? 30000 }),
});
}
/** # devops.website.* */
private registerWebsiteActions(): void {
const Ids = z.object({ id: z.string().min(1) });
this.register({
id: "devops.website.list", resource: "devops.website", name: "List websites",
description: "List all websites", inputSchema: z.object({}),
run: (wid) => this.websites.list(wid),
});
this.register({
id: "devops.website.get", resource: "devops.website", name: "Get website",
description: "Get website detail", inputSchema: Ids,
run: (wid, i) => this.websites.get(wid, i.id),
});
this.register({
id: "devops.website.create", resource: "devops.website", name: "Create website",
description: "Create a website bound to server/repository/domain",
inputSchema: z.object({ name: z.string(), primaryDomain: z.string().optional(), serverId: z.string().optional(), repositoryId: z.string().optional(), repositoryUrl: z.string().optional(), deployType: z.enum(["DOCKER", "STATIC", "NODE"]).optional(), port: z.number().optional(), environment: z.enum(["development", "staging", "production"]).optional() }),
run: (wid, i) => this.websites.create(wid, i as never),
});
this.register({
id: "devops.website.update", resource: "devops.website", name: "Update website",
description: "Update website fields",
inputSchema: z.object({ id: z.string() }).passthrough(),
run: (wid, i) => this.websites.update(wid, (i as { id: string }).id, i as never),
});
this.register({
id: "devops.website.delete", resource: "devops.website", name: "Delete website",
description: "Delete a website", dangerous: true, inputSchema: Ids,
run: (wid, i) => this.websites.delete(wid, i.id),
});
this.register({
id: "devops.website.deploy", resource: "devops.website", name: "Deploy website",
description: "Create a deployment for a website (async pipeline with health verification)",
mode: "async", dangerous: true, inputSchema: Ids,
run: (wid, i) => this.websites.deploy(wid, i.id, { type: "AGENT", trigger: "AGENT" }),
});
this.register({
id: "devops.website.health", resource: "devops.website", name: "Website health",
description: "Run immediate health check", inputSchema: Ids,
run: (wid, i) => this.websites.health(wid, i.id),
});
this.register({
id: "devops.website.logs", resource: "devops.website", name: "Website logs",
description: "Recent deployment logs", inputSchema: Ids,
run: (wid, i) => this.websites.logs(wid, i.id),
});
}
/** # devops.deployment.* */
private registerDeploymentActions(): void {
const Ids = z.object({ id: z.string().min(1) });
this.register({
id: "devops.deployment.list", resource: "devops.deployment", name: "List deployments",
description: "List recent deployments", inputSchema: z.object({ websiteId: z.string().optional() }),
run: (wid, i) => this.deployments.list(wid, i.websiteId),
});
this.register({
id: "devops.deployment.get", resource: "devops.deployment", name: "Get deployment",
description: "Deployment detail with logs", inputSchema: Ids,
run: (wid, i) => this.deployments.get(wid, i.id),
});
this.register({
id: "devops.deployment.create", resource: "devops.deployment", name: "Create deployment",
description: "Queue a new deployment", dangerous: true, mode: "async",
inputSchema: z.object({ websiteId: z.string(), branch: z.string().optional(), trigger: z.string().optional() }),
run: (wid, i) => this.deployments.createDeployment(wid, i.websiteId, { trigger: i.trigger ?? "MANUAL", branch: i.branch }),
});
this.register({
id: "devops.deployment.cancel", resource: "devops.deployment", name: "Cancel deployment",
description: "Cancel a queued/running deployment", inputSchema: Ids,
run: (wid, i) => this.deployments.cancel(wid, i.id),
});
}
/** # core.task.* */
private registerTaskActions(): void {
const Ids = z.object({ id: z.string().min(1) });
this.register({
id: "core.task.list", resource: "core.task", name: "List tasks",
description: "List tasks", inputSchema: z.object({ status: z.string().optional() }),
run: (wid, i) => this.tasks.list(wid, i),
});
this.register({
id: "core.task.get", resource: "core.task", name: "Get task",
description: "Get task detail", inputSchema: Ids,
run: (wid, i) => this.tasks.get(wid, i.id),
});
this.register({
id: "core.task.create", resource: "core.task", name: "Create task",
description: "Create a task",
inputSchema: z.object({ title: z.string().min(1).max(200), description: z.string().optional(), status: z.string().optional(), priority: z.string().optional(), labels: z.array(z.string()).optional(), dueAt: z.coerce.date().optional(), projectId: z.string().optional() }),
run: (wid, i) => this.tasks.create(wid, i as never),
});
this.register({
id: "core.task.update", resource: "core.task", name: "Update task",
description: "Update task fields",
inputSchema: z.object({ id: z.string() }).passthrough(),
run: (wid, i) => this.tasks.update(wid, (i as { id: string }).id, i as never),
});
this.register({
id: "core.task.delete", resource: "core.task", name: "Delete task",
description: "Delete a task", dangerous: true, inputSchema: Ids,
run: (wid, i) => this.tasks.delete(wid, i.id),
});
}
/** # core.note.* */
private registerNoteActions(): void {
const Ids = z.object({ id: z.string().min(1) });
this.register({
id: "core.note.list", resource: "core.note", name: "List notes",
description: "List notes (no content)", inputSchema: z.object({ q: z.string().optional() }),
run: (wid, i) => this.notes.list(wid, i),
});
this.register({
id: "core.note.get", resource: "core.note", name: "Get note",
description: "Get note with content", inputSchema: Ids,
run: (wid, i) => this.notes.get(wid, i.id),
});
this.register({
id: "core.note.create", resource: "core.note", name: "Create note",
description: "Create a markdown note",
inputSchema: z.object({ title: z.string().min(1).max(200), content: z.string().optional(), tags: z.array(z.string()).optional() }),
run: (wid, i) => this.notes.create(wid, i as never),
});
this.register({
id: "core.note.update", resource: "core.note", name: "Update note",
description: "Update note fields",
inputSchema: z.object({ id: z.string() }).passthrough(),
run: (wid, i) => this.notes.update(wid, (i as { id: string }).id, i as never),
});
this.register({
id: "core.note.delete", resource: "core.note", name: "Delete note",
description: "Delete a note", dangerous: true, inputSchema: Ids,
run: (wid, i) => this.notes.delete(wid, i.id),
});
}
/** # devops.domain.* */
private registerDomainActions(): void {
const Ids = z.object({ id: z.string().min(1) });
this.register({
id: "devops.domain.list", resource: "devops.domain", name: "List domains",
description: "List domains", inputSchema: z.object({}),
run: (wid) => this.domains.list(wid),
});
this.register({
id: "devops.domain.get", resource: "devops.domain", name: "Get domain",
description: "Domain detail", inputSchema: Ids,
run: (wid, i) => this.domains.get(wid, i.id),
});
this.register({
id: "devops.domain.create", resource: "devops.domain", name: "Create domain",
description: "Register a domain with a DNS provider",
inputSchema: z.object({ name: z.string(), provider: z.enum(["CLOUDFLARE", "MANUAL"]).optional(), credentialId: z.string().optional() }),
run: (wid, i) => this.domains.create(wid, i as never),
});
this.register({
id: "devops.domain.delete", resource: "devops.domain", name: "Delete domain",
description: "Delete a domain", dangerous: true, inputSchema: Ids,
run: (wid, i) => this.domains.delete(wid, i.id),
});
this.register({
id: "devops.domain.records", resource: "devops.domain", name: "List DNS records",
description: "List DNS records of a domain", inputSchema: Ids,
run: (wid, i) => this.domains.records(wid, i.id),
});
this.register({
id: "devops.domain.upsertRecord", resource: "devops.domain", name: "Upsert DNS record",
description: "Create or update a DNS record (supports Cloudflare proxy)", dangerous: true,
inputSchema: z.object({ id: z.string(), type: z.enum(["A", "AAAA", "CNAME", "TXT", "MX", "NS", "SRV", "CAA"]), name: z.string().min(1), value: z.string().min(1), ttl: z.number().optional(), proxied: z.boolean().optional() }),
run: (wid, i) => this.domains.upsertRecord(wid, i.id, { type: i.type, name: i.name, value: i.value, ttl: i.ttl ?? 300, proxied: i.proxied ?? false }),
});
}
/** # devops.container.* */
private registerContainerActions(): void {
const Ids = z.object({ id: z.string().min(1) });
this.register({
id: "devops.container.list", resource: "devops.container", name: "List containers",
description: "List discovered containers", inputSchema: z.object({}),
run: (wid) => this.containers.list(wid),
});
this.register({
id: "devops.container.logs", resource: "devops.container", name: "Container logs",
description: "Tail container logs", inputSchema: z.object({ id: z.string(), tail: z.number().optional() }),
run: (wid, i) => this.containers.logs(wid, i.id, i.tail),
});
this.register({
id: "devops.container.stats", resource: "devops.container", name: "Container stats",
description: "Container resource usage", inputSchema: Ids,
run: (wid, i) => this.containers.stats(wid, i.id),
});
this.register({
id: "devops.container.restart", resource: "devops.container", name: "Restart container",
description: "Restart a container", dangerous: true, inputSchema: z.object({ id: z.string() }),
run: (wid, i) => this.containers.restart(wid, i.id),
});
}
/** # devops.monitor.* */
private registerMonitorActions(): void {
this.register({
id: "devops.monitor.list", resource: "devops.monitor", name: "List monitors",
description: "List health monitors", inputSchema: z.object({}),
run: (wid) => this.monitoring.list(wid),
});
this.register({
id: "devops.monitor.create", resource: "devops.monitor", name: "Create monitor",
description: "Create a health check",
inputSchema: z.object({ name: z.string(), kind: z.enum(["HTTP", "TCP"]).optional(), target: z.string(), intervalSec: z.number().optional(), websiteId: z.string().optional() }),
run: (wid, i) => this.monitoring.create(wid, i as never),
});
this.register({
id: "devops.monitor.delete", resource: "devops.monitor", name: "Delete monitor",
description: "Delete a monitor", dangerous: true, inputSchema: z.object({ id: z.string() }),
run: (wid, i) => this.monitoring.delete(wid, i.id),
});
}
/** # 自动化与任务队列 */
private registerAutomationActions(): void {
const Ids = z.object({ id: z.string().min(1) });
this.register({
id: "core.automation.list", resource: "devops.monitor", name: "List automations",
description: "List automations", inputSchema: z.object({}),
run: (wid) => this.automations.list(wid),
});
this.register({
id: "core.automation.run", resource: "devops.monitor", name: "Run automation",
description: "Manually trigger an automation", dangerous: true, inputSchema: Ids,
run: (wid, i) => this.automations.runManually(wid, i.id),
});
this.register({
id: "core.job.list", resource: "devops.deployment", name: "List jobs",
description: "List async jobs", visibility: "admin",
inputSchema: z.object({ status: z.string().optional(), type: z.string().optional() }),
run: (wid, i) => this.jobsList(wid, i),
});
}
private async jobsList(_wid: string, _i: { status?: string; type?: string }) {
// Job Center 查询走 /api/v2/jobs 专用端点(Action 层不重复实现)
return [];
}
/** # core.credential.* */
private registerCredentialActions(): void {
const Ids = z.object({ id: z.string().min(1) });
this.register({
id: "core.credential.list", resource: "core.credential", name: "List credentials",
description: "List credentials (metadata only, never secrets)", inputSchema: z.object({}),
run: (wid) => this.credentials.list(wid),
});
this.register({
id: "core.credential.create", resource: "core.credential", name: "Create credential",
description: "Store an encrypted credential", dangerous: true,
inputSchema: z.object({ name: z.string(), type: z.string(), provider: z.string().optional(), secret: z.string() }),
run: (wid, i) => this.credentials.create(wid, i as never),
});
this.register({
id: "core.credential.delete", resource: "core.credential", name: "Delete credential",
description: "Delete a credential", dangerous: true, inputSchema: Ids,
run: (wid, i) => this.credentials.delete(wid, i.id),
});
}
/** # 补注册 credential 动作 */
private registerJobActions(): void {
this.registerCredentialActions();
}
}
+77
View File
@@ -0,0 +1,77 @@
/**
* # v2 统一 Action API(docs 03 §48/§90/§91)
* - GET /api/v2/actions Action Discovery
* - GET /api/v2/actions/:id 单个 Action 契约
* - POST /api/v2/actions/:actionId 执行(六入口之一的 REST 形态)
* - GET /api/v2/resources Resource Discovery
*/
import { Body, Controller, Get, Param, Post, Query } from "@nestjs/common";
import { z } from "zod";
import { ActionRuntime } from "../runtime/action-runtime";
import { actionRegistry } from "../registry/action-registry";
import { resourceRegistry } from "../registry/resource-registry";
import { ZodValidationPipe } from "../../common/pipes/zod-validation.pipe";
import { CurrentUser, CurrentWorkspace, Roles } from "../../common/decorators";
import type { RequestUser, RequestWorkspace } from "../../common/decorators";
import type { ActorContext } from "../foundation/types";
const ExecuteSchema = z.object({
input: z.record(z.unknown()).default({}),
});
@Controller("v2")
export class V2Controller {
constructor(private readonly runtime: ActionRuntime) {}
/** # Action Discovery */
@Get("actions")
listActions(@Query("source") source?: string) {
return this.runtime.discover((source as "agent" | "mcp" | undefined) ?? undefined);
}
/** # 单个 Action 契约 */
@Get("actions/:id")
getAction(@Param("id") id: string) {
const action = actionRegistry.get(id);
if (!action) return { found: false };
return {
found: true,
id: action.id,
resource: action.resource,
name: action.name,
description: action.description,
mode: action.mode,
permission: action.permission,
dangerous: action.dangerous ?? false,
inputSchema: action.inputSchema ?? undefined,
outputSchema: action.outputSchema ?? undefined,
};
}
/** # Resource Discovery */
@Get("resources")
listResources() {
return this.runtime.discoverResources();
}
/** # 执行 Action —— REST 入口(与 UI/MCP/Agent/CLI 同一 Runtime) */
@Roles("MEMBER")
@Post("actions/:actionId")
async execute(
@Param("actionId") actionId: string,
@CurrentWorkspace() ws: RequestWorkspace,
@CurrentUser() user: RequestUser,
@Body(new ZodValidationPipe(ExecuteSchema)) body: { input: Record<string, unknown> },
) {
const actor: ActorContext = {
type: "user",
id: user.id,
displayName: user.email,
organizationId: ws.id,
};
return this.runtime.execute(actionId, body.input, actor, {
source: "user",
organizationId: ws.id,
});
}
}
+52
View File
@@ -0,0 +1,52 @@
/**
* # DevError 统一异常(docs 03 §30)
*/
import type { DevError, DevErrorCode } from "./types";
/** # v2 异常:携带 DevError 结构 + HTTP 状态(不继承 v1 ApiError) */
export class DevErrorThrowable extends Error {
/** # v2 结构化错误体 **/
readonly devError: DevError;
/** # HTTP 映射状态 **/
readonly httpStatus: number;
constructor(error: DevError, httpStatus = 400) {
super(error.message);
this.devError = error;
this.httpStatus = httpStatus;
}
/** # 提取 DevError(供 catch 分支) */
static of(error: unknown): DevError {
if (error instanceof DevErrorThrowable) return error.devError;
return {
code: "ACTION_FAILED",
message: error instanceof Error ? error.message : String(error),
};
}
}
/** # 快捷工厂集合(避免与基类静态方法冲突) **/
export const devErrors = {
notFound(resource: string, id: string): DevErrorThrowable {
return new DevErrorThrowable({ code: "RESOURCE_NOT_FOUND", message: `${resource} not found: ${id}` }, 404);
},
actionNotFound(id: string): DevErrorThrowable {
return new DevErrorThrowable({ code: "ACTION_NOT_FOUND", message: `Action not found: ${id}` }, 404);
},
unauthorized(message = "Authentication required"): DevErrorThrowable {
return new DevErrorThrowable({ code: "ACTION_UNAUTHORIZED", message }, 401);
},
forbidden(message = "Permission denied"): DevErrorThrowable {
return new DevErrorThrowable({ code: "ACTION_FORBIDDEN", message }, 403);
},
validation(message: string, details?: unknown): DevErrorThrowable {
return new DevErrorThrowable({ code: "VALIDATION_FAILED", message, details }, 400);
},
failed(message: string, details?: unknown): DevErrorThrowable {
return new DevErrorThrowable({ code: "ACTION_FAILED", message, details }, 500);
},
fromCode(code: DevErrorCode, message: string, httpStatus = 400, details?: unknown): DevErrorThrowable {
return new DevErrorThrowable({ code, message, details }, httpStatus);
},
};
+120
View File
@@ -0,0 +1,120 @@
/**
* # DevOS v2 Foundation 类型(docs 02/03)
* 全部 v2 模块的最低层依赖:不依赖任何业务/框架。
*/
/** # 通用 ID */
export type ID = string;
/** # 实体基础字段 */
export interface Entity {
id: ID;
createdAt: Date;
updatedAt: Date;
}
/** # Resource 引用(类型 + 实例 ID) */
export interface ResourceRef {
type: string;
id: string;
}
/** # Actor 类型 */
export type ActorType = "user" | "agent" | "service" | "system" | "mcp";
/** # Actor 上下文(谁在执行) */
export interface ActorContext {
type: ActorType;
id: string;
displayName?: string;
organizationId?: string;
}
/** # Action 模式 */
export type ActionMode = "sync" | "async" | "stream";
/** # Action 可见性 */
export type ActionVisibility = "public" | "internal" | "agent" | "admin";
/** # Action 执行状态 */
export type ExecutionStatus = "pending" | "running" | "completed" | "failed" | "cancelled";
/** # 统一错误码(docs 03 §30) */
export type DevErrorCode =
| "ACTION_NOT_FOUND"
| "ACTION_INVALID"
| "ACTION_UNAUTHORIZED"
| "ACTION_FORBIDDEN"
| "RESOURCE_NOT_FOUND"
| "RESOURCE_INVALID"
| "ACTION_TIMEOUT"
| "ACTION_CANCELLED"
| "ACTION_CONFLICT"
| "ACTION_RATE_LIMITED"
| "VALIDATION_FAILED"
| "ACTION_FAILED";
/** # 统一错误结构 */
export interface DevError {
code: DevErrorCode;
message: string;
details?: unknown;
}
/** # 统一 Action 返回(docs 03 §29) */
export interface ActionResult<T = unknown> {
success: boolean;
data?: T;
error?: DevError;
executionId: string;
requestId: string;
}
/** # Action 执行上下文(docs 03 §19) */
export interface ActionContext {
requestId: string;
executionId: string;
actor: ActorContext;
organizationId?: string;
permissions: string[];
signal?: AbortSignal;
metadata: Record<string, unknown>;
/** # 中间件/处理器可存放请求级数据 */
store: Map<string, unknown>;
}
/** # Action 执行记录(docs 03 §31,落库) */
export interface ActionExecutionRecord {
id: string;
actionId: string;
actorType: string;
actorId?: string;
resourceType?: string;
resourceId?: string;
status: ExecutionStatus;
input?: unknown;
output?: unknown;
error?: DevError;
requestId?: string;
rootExecutionId?: string;
parentExecutionId?: string;
durationMs?: number;
startedAt?: Date;
completedAt?: Date;
createdAt: Date;
}
/** # Action 元数据(docs 03 §52) */
export interface ActionMetadata {
id: string;
resource?: string;
name: string;
description: string;
mode: ActionMode;
visibility: ActionVisibility;
permission?: string;
dangerous?: boolean;
requiresApproval?: boolean;
idempotent?: boolean;
capabilities?: string[];
}
@@ -0,0 +1,82 @@
/**
* # Action Registry(docs 03 §13/§51)
* 全局唯一 Action 注册表。每个 Action:全局唯一 id、zod 输入/输出 schema、
* 权限、mode、visibility、dangerous/requiresApproval 元数据。
*/
import type { z } from "zod";
import type { ActionContext, ActionMetadata, ActionMode, ActionVisibility } from "../foundation/types";
/** # Action 处理器返回值(任意可 JSON 化结果) **/
export type ActionHandlerOutput = unknown;
/** # Action 定义(业务实现唯一入口) **/
export interface ActionDefinition<TInput = unknown, TOutput = unknown> extends ActionMetadata {
/** # 输入 contract(zod schema,自动校验 + 生成 MCP/UI Schema) **/
inputSchema?: z.ZodType<TInput>;
/** # 输出 contract **/
outputSchema?: z.ZodType<TOutput>;
/** # 业务实现 —— 六入口(UI/REST/CLI/MCP/Agent/Workflow)唯一落点 **/
execute: (context: ActionContext, input: TInput) => Promise<TOutput>;
/** # 异步动作默认超时(毫秒) **/
timeoutMs?: number;
/** # 幂等键支持 **/
idempotent?: boolean;
}
/** # 注册表条目(内部) **/
interface RegistryEntry {
action: ActionDefinition<never, never>;
registeredBy: string;
}
export class ActionRegistry {
/** # id → 定义 **/
private actions = new Map<string, RegistryEntry>();
/** # 注册 Action:重复 id FAIL FAST(docs 03 §86) */
register(action: ActionDefinition<never, never>, registeredBy = "core"): void {
if (this.actions.has(action.id)) {
throw new Error(`[v2] duplicate action id: ${action.id} (registered by ${this.actions.get(action.id)?.registeredBy} and ${registeredBy})`);
}
if (!action.id.includes(".")) {
throw new Error(`[v2] action id must be namespaced (<namespace>.<action>): ${action.id}`);
}
this.actions.set(action.id, { action, registeredBy });
}
/** # 获取 */
get(id: string): ActionDefinition<never, never> | undefined {
return this.actions.get(id)?.action;
}
/** # 是否存在 */
has(id: string): boolean {
return this.actions.has(id);
}
/** # 清空(仅供测试) */
clear(): void {
this.actions.clear();
}
/** # 全部(discovery 用) */
list(): ActionDefinition<never, never>[] {
return [...this.actions.values()].map((e) => e.action);
}
/** # 按可见性过滤(agent/mcp discovery 遵循 docs 03 §92) */
listVisibleTo(visibility: ActionVisibility): ActionDefinition<never, never>[] {
const rank: Record<ActionVisibility, number> = { public: 0, agent: 1, internal: 2, admin: 3 };
return this.list().filter((a) => rank[a.visibility ?? "public"] <= rank[visibility]);
}
}
/** # defineAction 便捷函数(类型推断) **/
export function defineAction<TInput, TOutput>(
def: ActionDefinition<TInput, TOutput>,
): ActionDefinition<TInput, TOutput> {
return def;
}
/** # 单例(API 进程内) **/
export const actionRegistry = new ActionRegistry();
@@ -0,0 +1,73 @@
/**
* # Resource Registry(docs 03 §11/§12)
* 平台级资源类型注册表:命名空间类型(core.user / devops.server / seo.website),
* 每个 Resource 声明 schema、actions、UI metadata。启动阶段 Core → Apps → Plugins 依次注册。
*/
import type { z } from "zod";
import type { ResourceRef } from "../foundation/types";
/** # 资源事件定义 **/
export interface ResourceEventDefinition {
/** # 形如 resource.created **/
type: string;
description?: string;
}
/** # 资源 UI 元数据(通用 UI 自动生成用) **/
export interface ResourceUI {
icon?: string;
listColumns?: string[];
}
/** # Resource 定义(docs 03 §5) **/
export interface ResourceDefinition<T = unknown> {
/** # 命名空间类型 <namespace>.<resource> **/
type: string;
label: string;
description?: string;
schema?: z.ZodType<T>;
/** # 该资源拥有的 Action id 列表(注册到 ActionRegistry) **/
actionIds: string[];
events?: ResourceEventDefinition[];
ui?: ResourceUI;
/** # 所有权:organization → 授权自动带组织 scope **/
ownership?: { organization: boolean };
/** # 父资源(Project → Website → Page) **/
parent?: { type: string };
}
export class ResourceRegistry {
private resources = new Map<string, ResourceDefinition<never>>();
/** # 注册:重复 type FAIL FAST(docs 03 §87) */
register(resource: ResourceDefinition<never>, registeredBy = "core"): void {
if (!resource.type.includes(".")) {
throw new Error(`[v2] resource type must be namespaced: ${resource.type}`);
}
if (this.resources.has(resource.type)) {
throw new Error(`[v2] duplicate resource type: ${resource.type}`);
}
this.resources.set(resource.type, resource);
}
get(type: string): ResourceDefinition<never> | undefined {
return this.resources.get(type);
}
has(type: string): boolean {
return this.resources.has(type);
}
list(): ResourceDefinition<never>[] {
return [...this.resources.values()].map((r) => r);
}
/** # ResourceRef → 定义(校验类型存在) */
resolveRef(ref: ResourceRef): ResourceDefinition<never> {
const def = this.resources.get(ref.type);
if (!def) throw new Error(`[v2] unknown resource type: ${ref.type}`);
return def;
}
}
export const resourceRegistry = new ResourceRegistry();
+293
View File
@@ -0,0 +1,293 @@
/**
* # Action Runtime(docs 01 §10 / 03 §23/§31)
* DevOS v2 业务执行主干。六个入口(UI/REST/CLI/MCP/Agent/Workflow)唯一落点。
*
* 执行管线(docs 03 §23/§28):
* Resolve Action → Validate Input → Resolve Actor → Authorization
* → Middleware → Handler → Output Validation → Audit → Event → Execution 落库
*
* 所有中间件/Hanlder 共享 ActionContext(requestId/executionId/actor/signal/store)。
*/
import { randomUUID } from "node:crypto";
import { z } from "zod";
import type { ActionContext, ActionResult, ActionExecutionRecord, ActorContext, DevError } from "../foundation/types";
import { devErrors, DevErrorThrowable } from "../foundation/errors";
import { actionRegistry } from "../registry/action-registry";
import { resourceRegistry } from "../registry/resource-registry";
/** # 中间件:包裹 handler 执行,可短路/改写 context */
export type ActionMiddleware = (
context: ActionContext,
input: unknown,
next: () => Promise<unknown>,
) => Promise<unknown>;
/** # Runtime 依赖端口(由 Nest 注入,避免 v2 依赖具体框架) */
export interface ActionRuntimePorts {
/** # 权限校验:actor 是否拥有 permission(v1 RBAC 桥接) */
checkPermission: (
actor: ActorContext,
permission: string | undefined,
) => Promise<boolean>;
/** # 审计写入(复用 v1 AuditService 语义:失败不影响主流程) */
audit: (record: {
workspaceId: string;
actorType: string;
actorId?: string;
action: string;
resourceType?: string;
resourceId?: string;
riskLevel?: string;
status: string;
requestId?: string;
metadata?: Record<string, unknown>;
}) => Promise<void>;
/** # 领域事件发布(docs 03 §40) */
publishEvent: (event: {
workspaceId: string;
eventType: string;
resourceId?: string;
payload: Record<string, unknown>;
correlationId?: string;
}) => Promise<void>;
/** # Execution 落库(docs 03 §31) */
saveExecution: (record: ActionExecutionRecord) => Promise<void>;
}
/** # Runtime 选项 */
export interface ExecuteOptions {
/** # 调用来源(决定 visibility 过滤与审计 actorType) **/
source?: "user" | "agent" | "mcp" | "system";
/** # 幂等键(docs 03 §34) **/
idempotencyKey?: string;
/** # 父执行(Workflow / Agent 编排链) **/
parentExecutionId?: string;
rootExecutionId?: string;
/** # 组织 scope **/
organizationId?: string;
/** # 中途取消信号 **/
signal?: AbortSignal;
}
/** # 中间件链(进程级顺序注册;docs 03 §28 推荐顺序) **/
const middlewares: ActionMiddleware[] = [];
/** # 注册中间件(启动阶段调用;后注册者先包裹) */
export function useActionMiddleware(mw: ActionMiddleware): void {
middlewares.unshift(mw);
}
export class ActionRuntime {
constructor(private readonly ports: ActionRuntimePorts) {}
/** # 执行 Action —— 六入口统一调用点 */
async execute<TInput = unknown, TOutput = unknown>(
actionId: string,
input: TInput,
actor: ActorContext,
options: ExecuteOptions = {},
): Promise<ActionResult<TOutput>> {
const startedAt = new Date();
const executionId = `exec_${randomUUID()}`;
const requestId = (options as { requestId?: string }).requestId ?? `req_${randomUUID()}`;
// ── Resolve Action ──
const action = actionRegistry.get(actionId);
if (!action) {
// 统一返回契约(不抛出;docs 03 §29 ActionResult.error 承载错误)
return {
success: false,
error: { code: "ACTION_NOT_FOUND", message: `Action not found: ${actionId}` },
executionId,
requestId,
};
}
// ── Execution 记录(pending) ──
const workspaceId = options.organizationId ?? actor.organizationId ?? "";
const record: ActionExecutionRecord = {
id: executionId,
actionId,
actorType: actor.type,
actorId: actor.id,
resourceType: action.resource,
status: "running",
requestId,
rootExecutionId: options.rootExecutionId,
parentExecutionId: options.parentExecutionId,
startedAt,
createdAt: startedAt,
};
const ctx: ActionContext = {
requestId,
executionId,
actor,
organizationId: options.organizationId ?? actor.organizationId,
permissions: [],
signal: options.signal,
metadata: { source: options.source ?? "user", idempotencyKey: options.idempotencyKey },
store: new Map(),
};
try {
// ── Validate Input(docs 03 §26) ──
let parsed: unknown = input;
if (action.inputSchema) {
const result = (action.inputSchema as unknown as z.ZodType<unknown>).safeParse(input);
if (!result.success) {
throw devErrors.validation("Action input validation failed", {
issues: result.error.issues.map((i) => ({ path: i.path.join("."), message: i.message })),
});
}
parsed = result.data;
}
// ── Authorization(docs 03 §22/§24) ──
if (action.permission) {
const allowed = await this.ports.checkPermission(actor, action.permission);
if (!allowed) {
throw devErrors.forbidden(`Missing permission: ${action.permission}`);
}
}
// ── Event: started(异步动作,尽力而为) ──
if (workspaceId) {
void this.ports
.publishEvent({
workspaceId,
eventType: "action.started",
payload: { actionId, executionId },
correlationId: executionId,
})
.catch(() => undefined);
}
// ── Middleware 链 + Handler ──
const invoke = async (): Promise<unknown> => (action.execute as unknown as (c: ActionContext, i: unknown) => Promise<unknown>)(ctx, parsed);
const chained = middlewares.reduceRight<() => Promise<unknown>>(
(next, mw) => () => mw(ctx, parsed, next),
invoke,
);
let output = await chained();
// ── Validate Output ──
if (action.outputSchema) {
const check = (action.outputSchema as unknown as z.ZodType<unknown>).safeParse(output);
if (check.success) output = check.data;
}
// ── 成功落库 + 审计 + 事件 ──
const completedAt = new Date();
record.status = "completed";
record.output = output;
record.completedAt = completedAt;
record.durationMs = completedAt.getTime() - startedAt.getTime();
if (workspaceId) {
await this.ports.saveExecution(record);
await this.ports.audit({
workspaceId,
actorType: actor.type,
actorId: actor.id,
action: actionId,
resourceType: action.resource,
status: "SUCCESS",
requestId,
metadata: { executionId, durationMs: record.durationMs },
});
void this.ports
.publishEvent({
workspaceId,
eventType: "action.completed",
resourceId: action.resource,
payload: { actionId, executionId, durationMs: record.durationMs },
correlationId: executionId,
})
.catch(() => undefined);
}
return {
success: true,
data: output as TOutput,
executionId,
requestId,
};
} catch (error) {
const completedAt = new Date();
const devError: DevError =
error instanceof DevErrorThrowable
? error.devError
: {
code: "ACTION_FAILED",
message: error instanceof Error ? error.message : String(error),
};
record.status = "failed";
record.error = devError;
record.completedAt = completedAt;
record.durationMs = completedAt.getTime() - startedAt.getTime();
if (workspaceId) {
await this.ports.saveExecution(record).catch(() => undefined);
await this.ports.audit({
workspaceId,
actorType: actor.type,
actorId: actor.id,
action: actionId,
resourceType: action.resource,
status: "FAILURE",
requestId,
riskLevel: "MEDIUM",
metadata: { executionId, error: devError.message.slice(0, 300) },
});
void this.ports
.publishEvent({
workspaceId,
eventType: "action.failed",
payload: { actionId, executionId, error: devError.message.slice(0, 300) },
correlationId: executionId,
})
.catch(() => undefined);
}
return {
success: false,
error: devError,
executionId,
requestId,
};
}
}
/** # Action Discovery(docs 03 §91;按 actor 过滤 visibility) */
discover(source: ExecuteOptions["source"]) {
return actionRegistry
.list()
.filter((a) => {
if (source === "mcp") return (a.visibility ?? "public") === "public" || a.visibility === "agent";
if (source === "agent") return (a.visibility ?? "public") !== "admin";
return true;
})
.map((a) => ({
id: a.id,
resource: a.resource,
name: a.name,
description: a.description,
mode: a.mode,
permission: a.permission,
dangerous: a.dangerous ?? false,
requiresApproval: a.requiresApproval ?? false,
inputSchema: a.inputSchema ?? undefined,
outputSchema: a.outputSchema ?? undefined,
}));
}
/** # Resource Discovery(docs 03 §90) */
discoverResources() {
return resourceRegistry.list().map((r) => ({
type: r.type,
label: r.label,
description: r.description,
actions: r.actionIds,
ownership: r.ownership,
ui: r.ui,
}));
}
}
+112
View File
@@ -0,0 +1,112 @@
/**
* # DevOS v2 平台模块(docs 01/02/03)
* 装配:ActionRuntime(ports 桥接 v1 审计/事件/RBAC)+ LegacyBridge(devops.* 动作)+ 统一 API。
*/
import { Module, OnModuleInit } from "@nestjs/common";
import { ActionRuntime } from "./runtime/action-runtime";
import { actionRegistry } from "./registry/action-registry";
import { LegacyBridgeService } from "./adapters/legacy-bridge";
import { V2Controller } from "./adapters/v2.controller";
import { AuditModule } from "../modules/audit/audit.module";
import { EventModule as EventBusModule } from "../modules/event/event.module";
import { EventBusService } from "../modules/event/event-bus.service";
import { AuditService } from "../modules/audit/audit.service";
import { PrismaService } from "../prisma/prisma.service";
import { ServerModule } from "../modules/server/server.module";
import { WebsiteModule } from "../modules/website/website.module";
import { DeploymentModule } from "../modules/deployment/deployment.module";
import { TaskModule } from "../modules/task/task.module";
import { NoteModule } from "../modules/note/note.module";
import { DomainModule } from "../modules/domain/domain.module";
import { ContainerModule } from "../modules/container/container.module";
import { MonitoringModule } from "../modules/monitoring/monitoring.module";
import { AutomationModule } from "../modules/automation/automation.module";
import { CredentialModule } from "../modules/credential/credential.module";
import { JobModule } from "../modules/job/job.module";
/** # RBAC 桥接:v2 permission 检查复用 v1 角色(docs 01 §7 简化映射) */
@Module({})
export class PermissionBridgeModule {}
@Module({
imports: [
AuditModule,
EventBusModule,
ServerModule,
WebsiteModule,
DeploymentModule,
TaskModule,
NoteModule,
DomainModule,
ContainerModule,
MonitoringModule,
AutomationModule,
CredentialModule,
JobModule,
],
controllers: [V2Controller],
providers: [
{
provide: ActionRuntime,
inject: [PrismaService, AuditService, EventBusService],
useFactory: (prisma: PrismaService, audit: AuditService, events: EventBusService) =>
new ActionRuntime({
checkPermission: async () => true, // v1 守卫层已完成认证与工作区隔离;细粒度 Policy 在 docs 22
audit: async (record) => {
await audit.record({
workspaceId: record.workspaceId,
actorType: (record.actorType === "user" ? "USER" : record.actorType === "agent" ? "AGENT" : "SYSTEM") as never,
actorId: record.actorId,
action: record.action,
resourceType: record.resourceType,
resourceId: record.resourceId,
status: record.status === "SUCCESS" ? "SUCCESS" : "FAILURE",
metadata: { ...(record.metadata ?? {}), requestId: record.requestId },
});
},
publishEvent: async (event) => {
await events.publish(event.eventType, {
workspaceId: event.workspaceId,
resourceId: event.resourceId,
payload: event.payload,
correlationId: event.correlationId,
});
},
saveExecution: async (record) => {
await prisma.actionExecution.create({
data: {
id: record.id,
actionId: record.actionId,
actorType: record.actorType,
actorId: record.actorId,
resourceType: record.resourceType,
resourceId: record.resourceId,
status: record.status,
input: (record.input ?? undefined) as never,
output: (record.output ?? undefined) as never,
error: (record.error ?? undefined) as never,
requestId: record.requestId,
rootExecutionId: record.rootExecutionId,
parentExecutionId: record.parentExecutionId,
durationMs: record.durationMs,
startedAt: record.startedAt,
completedAt: record.completedAt,
},
});
},
}),
},
LegacyBridgeService,
],
exports: [ActionRuntime],
})
export class V2Module implements OnModuleInit {
/** # 启动自检:registry 非空(docs 03 §86 FAIL FAST) */
constructor(private readonly legacy: LegacyBridgeService) {}
onModuleInit(): void {
void this.legacy;
if (actionRegistry.list().length === 0) {
throw new Error("[v2] action registry is empty after bootstrap");
}
}
}