Inclui console handoff Desk↔Console (Spec 019), melhorias DNS Viewer (037), OpenPanel/Nextcloud/VM116 deploy notes, contracts stack e sidebar actualizado. Co-authored-by: Cursor <cursoragent@cursor.com>
131 lines
4.8 KiB
TypeScript
131 lines
4.8 KiB
TypeScript
/**
|
|
* Spec 037 — OnboardMaestro (Fase B)
|
|
* Durable Object: orquestra passos; delega agentes especializados (Fase C).
|
|
*/
|
|
import { DurableObject } from "cloudflare:workers";
|
|
import { vm112Get, vm112Post, type Vm112Config } from "./vm112-client";
|
|
|
|
export type OnboardState = {
|
|
sessionId: string;
|
|
domain: string;
|
|
step: string;
|
|
ligboxProjectEmail?: string;
|
|
projectId?: string;
|
|
dnsMode?: string;
|
|
awaitingHuman?: string | null;
|
|
history: string[];
|
|
};
|
|
|
|
export class OnboardMaestro extends DurableObject {
|
|
private state: OnboardState | null = null;
|
|
|
|
private cfg(): Vm112Config {
|
|
const env = this.env as { VM112_API_BASE: string };
|
|
if (!this.state?.sessionId) throw new Error("session not initialized");
|
|
return { baseUrl: env.VM112_API_BASE, sessionId: this.state.sessionId };
|
|
}
|
|
|
|
private log(msg: string) {
|
|
if (!this.state) return;
|
|
this.state.history.push(`${new Date().toISOString()} ${msg}`);
|
|
if (this.state.history.length > 100) this.state.history.shift();
|
|
}
|
|
|
|
async fetch(request: Request): Promise<Response> {
|
|
const url = new URL(request.url);
|
|
const path = url.pathname;
|
|
|
|
if (path === "/init" && request.method === "POST") {
|
|
const body = (await request.json()) as { sessionId: string; domain: string };
|
|
this.state = {
|
|
sessionId: body.sessionId,
|
|
domain: body.domain.toLowerCase(),
|
|
step: "init",
|
|
history: [],
|
|
awaitingHuman: null,
|
|
};
|
|
await this.ctx.storage.put("state", this.state);
|
|
this.log("initialized");
|
|
return Response.json({ ok: true, state: this.state });
|
|
}
|
|
|
|
const stored = (await this.ctx.storage.get<OnboardState>("state")) || this.state;
|
|
if (!stored) return Response.json({ error: "not initialized" }, { status: 400 });
|
|
this.state = stored;
|
|
|
|
if (path === "/state") {
|
|
return Response.json({ state: this.state });
|
|
}
|
|
|
|
if (path === "/run/proj-mail" && request.method === "POST") {
|
|
const data = await vm112Post(this.cfg(), "/onboarding/project/provision-email", {
|
|
domain: this.state.domain,
|
|
});
|
|
this.state.ligboxProjectEmail = data.ligbox_project_email as string;
|
|
this.state.projectId = data.project_id as string;
|
|
this.state.step = "proj_mail_done";
|
|
this.state.awaitingHuman = "reveal_password";
|
|
await this.ctx.storage.put("state", this.state);
|
|
this.log(`proj-mail ${this.state.ligboxProjectEmail}`);
|
|
return Response.json({ ok: true, data, state: this.state });
|
|
}
|
|
|
|
if (path === "/run/cf-zone" && request.method === "POST") {
|
|
const prov = await vm112Post(this.cfg(), "/onboarding/dns/cloudflare/provision-zone", {
|
|
domain: this.state.domain,
|
|
});
|
|
const apply = await vm112Post(this.cfg(), "/onboarding/dns/cloudflare/apply", {
|
|
domain: this.state.domain,
|
|
zone_id: prov.zone_id,
|
|
});
|
|
const verify = await vm112Get(this.cfg(), `/onboarding/dns/verify/${this.state.domain}`);
|
|
this.state.dnsMode = (apply.dns_mode as string) || "ligbox_cf";
|
|
this.state.step = "cf_zone_done";
|
|
await this.ctx.storage.put("state", this.state);
|
|
this.log("cf-zone apply+verify");
|
|
return Response.json({ ok: true, prov, apply, verify, state: this.state });
|
|
}
|
|
|
|
if (path === "/human/ack" && request.method === "POST") {
|
|
const body = (await request.json()) as { action: string };
|
|
if (this.state.awaitingHuman === body.action) {
|
|
this.state.awaitingHuman = null;
|
|
await this.ctx.storage.put("state", this.state);
|
|
}
|
|
return Response.json({ ok: true, state: this.state });
|
|
}
|
|
|
|
if (path === "/run/handoff" && request.method === "POST") {
|
|
const body = (await request.json()) as { manager_email: string };
|
|
const data = await vm112Post(this.cfg(), "/onboarding/dns/cloudflare/handoff-manager", {
|
|
domain: this.state.domain,
|
|
manager_email: body.manager_email,
|
|
});
|
|
this.state.step = "handoff_done";
|
|
await this.ctx.storage.put("state", this.state);
|
|
this.log(`handoff ${body.manager_email}`);
|
|
return Response.json({ ok: true, data, state: this.state });
|
|
}
|
|
|
|
return Response.json({ error: "not found" }, { status: 404 });
|
|
}
|
|
}
|
|
|
|
/** Fase C — agentes especializados (stubs; lógica no Maestro até split) */
|
|
export class ProjMailAgent extends DurableObject {
|
|
async fetch(request: Request): Promise<Response> {
|
|
return Response.json({ agent: "proj-mail", status: "delegate-to-maestro" });
|
|
}
|
|
}
|
|
|
|
export class CfZoneAgent extends DurableObject {
|
|
async fetch(request: Request): Promise<Response> {
|
|
return Response.json({ agent: "cf-zone", status: "delegate-to-maestro" });
|
|
}
|
|
}
|
|
|
|
export class CfHandoffAgent extends DurableObject {
|
|
async fetch(request: Request): Promise<Response> {
|
|
return Response.json({ agent: "cf-handoff", status: "delegate-to-maestro" });
|
|
}
|
|
}
|