feat: add D601 dev backend path

This commit is contained in:
Codex
2026-05-18 10:31:43 +00:00
parent 11b94f3a95
commit 37504a28e8
56 changed files with 10489 additions and 311 deletions
+37 -4
View File
@@ -4,7 +4,7 @@ import { isRebuildableService, rebuildService, stackLogs, stackStatus, startStac
import { parseE2ERunOptions, runE2E } from "./src/e2e";
import { emitError, emitJson } from "./src/output";
import { jobWithTail, listJobs, listJobsSummary, readJob, runJob } from "./src/jobs";
import { parseCheckOptions, runChecks } from "./src/check";
import { checkHelp, parseCheckOptions, runChecks } from "./src/check";
import { runSsh } from "./src/ssh";
import { extractRemoteCliOptions, runRemoteCli } from "./src/remote";
import { runMicroserviceCommand } from "./src/microservices";
@@ -30,13 +30,13 @@ function help(): unknown {
{ command: "help", description: "List supported commands." },
{ command: "--main-server-ip <ip> <command>", description: "Run selected commands through the public frontend API; use --main-server-key only for legacy SSH transport." },
{ command: "config show", description: "Validate and print config.json as the single source of truth." },
{ command: "check [--full|--files|--scripts-typecheck|--components|--compose|--logs]", description: "Run the lightweight default syntax/config gate; opt into file, type, Compose, or policy checks explicitly." },
{ command: "check [--full|--files|--scripts-typecheck|--components|--compose|--logs|--rust]", description: "Run the lightweight default syntax/config gate; Rust is opt-in and only allowed from D601 CI/dev execution." },
{ command: "server start", description: "Fire-and-forget build/start for database, backend-core, frontend, provider gateway, and managed main-server user services." },
{ command: "server stop", description: "Fire-and-forget docker-compose down for the fixed UniDesk stack." },
{ command: "server status", description: "Show fixed ports, containers, service health, and public URLs." },
{ command: "server swap status|ensure [--path /swapfile] [--size 2GiB] [--dry-run]", description: "Inspect or idempotently create host swap for low-memory main-server operation." },
{ command: "server logs [--tail-bytes N]", description: "Return bounded tails from file logs and docker logs." },
{ command: "server rebuild <backend-core|frontend|provider-gateway|todo-note|code-queue-mgr|project-manager|baidu-netdisk|oa-event-flow>", description: "Build first, then serialize, force-recreate, and validate one Compose service." },
{ command: "server rebuild <backend-core|frontend|dev-frontend-proxy|provider-gateway|todo-note|code-queue-mgr|project-manager|baidu-netdisk|oa-event-flow>", description: "Build first, then serialize, force-recreate, and validate one Compose service." },
{ command: "provider attach <providerId> [--master-server URL] [--up] [--force]", description: "Generate the minimal external provider-gateway env/compose bundle; only master server URL and provider id are required." },
{ command: "ssh <providerId> [ssh-like args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge with built-in remote helper tools in PATH." },
{ command: "ssh <providerId> apply-patch [tool args...] < patch.diff", description: "Invoke the injected remote apply_patch helper directly over SSH passthrough and stream the patch from local stdin." },
@@ -85,6 +85,31 @@ function isHelpToken(value: string | undefined): boolean {
return value === "help" || value === "--help" || value === "-h";
}
function serverHelp(action: string | undefined = undefined): unknown {
return {
command: action === undefined || isHelpToken(action) ? "server start|stop|status|swap|logs|rebuild" : `server ${action}`,
output: "json",
description: "Manage the fixed main-server Docker Compose stack without exposing backend-core REST publicly.",
usage: {
start: "bun scripts/cli.ts server start",
stop: "bun scripts/cli.ts server stop",
status: "bun scripts/cli.ts server status",
swap: "bun scripts/cli.ts server swap status|ensure [--path /swapfile] [--size 2GiB] [--dry-run]",
logs: "bun scripts/cli.ts server logs [--tail-bytes N]",
rebuild: "bun scripts/cli.ts server rebuild <backend-core|frontend|dev-frontend-proxy|provider-gateway|todo-note|code-queue-mgr|project-manager|baidu-netdisk|oa-event-flow>",
},
publicEntrypoints: {
frontend: "prod UniDesk frontend",
devFrontend: "dev UniDesk frontend proxy to D601 unidesk-dev/frontend-dev",
providerIngress: "provider-gateway WebSocket ingress",
},
rustBoundary: {
masterServer: "do not use server rebuild backend-core for Rust iteration; it would build locally",
d601: "use deploy apply --env dev --service backend-core and CI for Rust build/check",
},
};
}
function sshHelp(): unknown {
return {
command: "ssh",
@@ -206,6 +231,10 @@ async function main(): Promise<void> {
}
if (top === "check") {
if (isHelpToken(sub)) {
emitJson(commandName, checkHelp());
return;
}
const result = runChecks(config, parseCheckOptions(args.slice(1)));
emitJson(commandName, result, result.ok);
if (!result.ok) process.exitCode = 1;
@@ -213,6 +242,10 @@ async function main(): Promise<void> {
}
if (top === "server") {
if (isHelpToken(sub) || args.slice(2).some(isHelpToken)) {
emitJson(commandName, serverHelp(isHelpToken(sub) ? undefined : sub));
return;
}
if (sub === "start") {
emitJson(commandName, startStack(config));
return;
@@ -238,7 +271,7 @@ async function main(): Promise<void> {
}
if (sub === "rebuild") {
if (!isRebuildableService(third)) {
throw new Error("server rebuild requires one of: backend-core, frontend, provider-gateway, todo-note, code-queue-mgr, project-manager, baidu-netdisk, oa-event-flow");
throw new Error("server rebuild requires one of: backend-core, frontend, dev-frontend-proxy, provider-gateway, todo-note, code-queue-mgr, project-manager, baidu-netdisk, oa-event-flow");
}
emitJson(commandName, rebuildService(config, third));
return;
+68 -18
View File
@@ -21,7 +21,6 @@ const syntaxFiles = [
"scripts/src/docker.ts",
"scripts/src/e2e.ts",
"scripts/src/remote.ts",
"src/components/backend-core/src/index.ts",
"src/components/frontend/src/index.ts",
"src/components/frontend/src/app.tsx",
"src/components/frontend/src/decision-center.tsx",
@@ -40,6 +39,7 @@ export interface CheckOptions {
components: boolean;
compose: boolean;
logs: boolean;
rust: boolean;
}
const defaultCheckOptions: CheckOptions = {
@@ -49,8 +49,32 @@ const defaultCheckOptions: CheckOptions = {
components: false,
compose: false,
logs: false,
rust: false,
};
export function checkHelp(): Record<string, unknown> {
return {
ok: true,
command: "check",
usage: "bun scripts/cli.ts check [--syntax-only|--full|--files|--scripts-typecheck|--components|--compose|--logs|--rust]",
defaultMode: "syntax/config only; Rust is never compiled on the master server by default",
options: [
{ name: "--syntax-only|--basic", description: "Run only config validation, Bun version and TypeScript syntax transpile." },
{ name: "--full", description: "Enable all local checks except Rust compilation." },
{ name: "--files", description: "Verify required entrypoint files, including backend-core Cargo files." },
{ name: "--scripts-typecheck", description: "Run scripts TypeScript typecheck." },
{ name: "--components", description: "Run component TypeScript typecheck." },
{ name: "--compose", description: "Render Docker Compose config." },
{ name: "--logs", description: "Check unified log rotation policy." },
{ name: "--rust", description: "Run cargo check only when UNIDESK_D601_RUST_CHECK=1 is set inside D601 CI/dev execution." },
],
rustBoundary: {
masterServer: "do not run cargo check/build here",
d601: "use deploy apply --env dev --service backend-core and CI with UNIDESK_D601_RUST_CHECK=1",
},
};
}
export function parseCheckOptions(args: string[]): CheckOptions {
const options = { ...defaultCheckOptions };
for (const arg of args) {
@@ -71,15 +95,10 @@ export function parseCheckOptions(args: string[]): CheckOptions {
options.compose = true;
} else if (arg === "--logs") {
options.logs = true;
} else if (arg === "--rust") {
options.rust = true;
} else if (arg === "--basic" || arg === "--syntax-only") {
options.full = false;
options.files = false;
options.scriptsTypecheck = false;
options.components = false;
options.compose = false;
options.logs = false;
} else if (arg === "--help" || arg === "-h") {
throw new Error("check usage: bun scripts/cli.ts check [--syntax-only|--full|--files|--scripts-typecheck|--components|--compose|--logs]");
Object.assign(options, defaultCheckOptions);
} else {
throw new Error(`unknown check option: ${arg}`);
}
@@ -92,8 +111,8 @@ function fileItem(path: string): CheckItem {
return { name: `file:${path}`, ok: existsSync(absolute), detail: absolute };
}
function commandItem(name: string, command: string[], timeoutMs = 30_000): CheckItem {
const result = runCommand(command, repoRoot, { timeoutMs });
function commandItem(name: string, command: string[], timeoutMs = 30_000, env: NodeJS.ProcessEnv = process.env): CheckItem {
const result = runCommand(command, repoRoot, { timeoutMs, env });
return {
name,
ok: result.exitCode === 0,
@@ -133,9 +152,9 @@ function syntaxItem(): CheckItem {
function unifiedLogRotationItem(): CheckItem {
const serviceFiles = [
"src/components/backend-core/src/logger.ts",
"src/components/frontend/src/index.ts",
"src/components/provider-gateway/src/index.ts",
"src/components/microservices/code-queue-mgr/src/index.ts",
"src/components/microservices/code-queue/src/index.ts",
"src/components/microservices/k3sctl-adapter/src/index.ts",
"src/components/microservices/mdtodo/src/index.ts",
@@ -143,7 +162,6 @@ function unifiedLogRotationItem(): CheckItem {
"src/components/microservices/baidu-netdisk/src/index.ts",
"src/components/microservices/oa-event-flow/src/index.ts",
"src/components/microservices/decision-center/src/index.ts",
"src/components/microservices/code-queue-mgr/src/index.ts",
];
const offenders = serviceFiles.flatMap((path) => {
const text = readFileSync(rootPath(path), "utf8");
@@ -151,17 +169,41 @@ function unifiedLogRotationItem(): CheckItem {
const missingWriter = !text.includes("createHourlyJsonlWriter");
return directLogAppend || missingWriter ? [{ path, directLogAppend, missingWriter }] : [];
});
const backendLogger = readFileSync(rootPath("src/components/backend-core/src/logger.rs"), "utf8");
const backendMissingRotation = !backendLogger.includes("current_path") || !backendLogger.includes("prune");
const backendDirectUnboundedAppend = backendLogger.includes("appendFileSync");
if (backendMissingRotation || backendDirectUnboundedAppend) {
offenders.push({ path: "src/components/backend-core/src/logger.rs", directLogAppend: backendDirectUnboundedAppend, missingWriter: backendMissingRotation });
}
return {
name: "logs:unified-hourly-rotation",
ok: offenders.length === 0,
detail: {
sharedWriter: "src/components/shared/src/rotating-jsonl.ts",
checkedFiles: serviceFiles,
checkedFiles: ["src/components/backend-core/src/logger.rs", ...serviceFiles],
offenders,
},
};
}
function rustCheckItem(): CheckItem {
if (process.env.UNIDESK_D601_RUST_CHECK !== "1") {
return {
name: "rust:backend-core",
ok: false,
detail: {
skipped: true,
reason: "Rust compilation is intentionally not allowed on the master server; run it from D601 CI/dev deploy.",
enableOnD601: "UNIDESK_D601_RUST_CHECK=1 bun scripts/cli.ts check --rust",
deployPath: "bun scripts/cli.ts deploy apply --env dev --service backend-core",
},
};
}
const envPath = process.env.HOME ? `${process.env.HOME}/.cargo/bin:${process.env.PATH ?? ""}` : process.env.PATH;
const env = envPath ? { ...process.env, PATH: envPath } : process.env;
return commandItem("rust:backend-core", ["cargo", "check", "--manifest-path", "src/components/backend-core/Cargo.toml"], 180_000, env);
}
function skippedItem(name: string, reason: string, enableWith: string): CheckItem {
return { name, ok: true, detail: { skipped: true, reason, enableWith } };
}
@@ -178,7 +220,10 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
fileItem("AGENTS.md"),
fileItem("TEST.md"),
fileItem("docker-compose.yml"),
fileItem("src/components/backend-core/src/index.ts"),
fileItem("src/components/backend-core/Cargo.toml"),
fileItem("src/components/backend-core/Cargo.lock"),
fileItem("src/components/backend-core/src/main.rs"),
fileItem("src/components/backend-core/src/http.rs"),
fileItem("src/components/frontend/src/index.ts"),
fileItem("src/components/provider-gateway/src/index.ts"),
fileItem("src/components/microservices/oa-event-flow/src/index.ts"),
@@ -193,19 +238,19 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
items.push(skippedItem("files:required-entrypoints", "required file presence scan is opt-in", "--files or --full"));
}
if (options.scriptsTypecheck) {
items.push(commandItem("typescript:scripts", ["bunx", "tsc", "-p", "scripts/tsconfig.json", "--noEmit", "--pretty", "false"]));
items.push(commandItem("typescript:scripts", ["bunx", "tsc", "-p", "scripts/tsconfig.json", "--noEmit", "--pretty", "false"], 120_000));
} else {
items.push(skippedItem("typescript:scripts", "scripts TypeScript typecheck is opt-in", "--scripts-typecheck or --full"));
}
if (options.logs) {
items.push(unifiedLogRotationItem());
} else {
items.push(skippedItem("logs:unified-hourly-rotation", "heavy policy scan is opt-in", "--logs or --full"));
items.push(skippedItem("logs:unified-hourly-rotation", "policy scan is opt-in", "--logs or --full"));
}
if (options.components) {
items.push(commandItem("typescript:components", ["bunx", "tsc", "-p", "src/tsconfig.check.json", "--pretty", "false"], 180_000));
} else {
items.push(skippedItem("typescript:components", "full component TypeScript check is opt-in", "--components or --full"));
items.push(skippedItem("typescript:components", "component TypeScript check is opt-in", "--components or --full"));
}
if (options.compose) {
const compose = composeConfig(config);
@@ -225,5 +270,10 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
} else {
items.push(skippedItem("docker-compose:config", "Docker Compose config rendering is opt-in", "--compose or --full"));
}
if (options.rust) {
items.push(rustCheckItem());
} else {
items.push(skippedItem("rust:backend-core", "Rust check/build must run on D601 dev deploy or CI, not on the master server", "--rust inside D601 CI with UNIDESK_D601_RUST_CHECK=1"));
}
return { ok: items.every((item) => item.ok), mode: options.full ? "full" : "basic", options, items };
}
+5 -1
View File
@@ -104,6 +104,10 @@ function boolFlag(args: string[], name: string): boolean {
return args.includes(name);
}
function isHelpArg(value: string | undefined): boolean {
return value === "help" || value === "--help" || value === "-h";
}
function shellQuote(value: string): string {
return `'${value.replace(/'/gu, "'\\''")}'`;
}
@@ -811,7 +815,7 @@ function requireRunId(value: string): string {
export async function runCiCommand(_config: UniDeskConfig, args: string[]): Promise<Record<string, unknown>> {
const [action = "status", nameArg] = args;
if (action === "help" || action === "--help" || action === "-h") return help();
if (isHelpArg(action) || args.slice(1).some(isHelpArg)) return help();
if (action === "install") return install();
if (action === "status") return status();
if (action === "run") {
+2 -1
View File
@@ -11,10 +11,11 @@ export interface CommandResult {
timedOut: boolean;
}
export function runCommand(command: string[], cwd: string, options: { timeoutMs?: number } = {}): CommandResult {
export function runCommand(command: string[], cwd: string, options: { timeoutMs?: number; env?: NodeJS.ProcessEnv } = {}): CommandResult {
const result = spawnSync(command[0], command.slice(1), {
cwd,
encoding: "utf8",
env: options.env,
maxBuffer: 1024 * 1024 * 8,
timeout: options.timeoutMs,
});
+2
View File
@@ -10,6 +10,7 @@ export interface UniDeskConfig {
publicHost: string;
core: { port: number; containerPort: number };
frontend: { port: number; containerPort: number };
devFrontend: { port: number; containerPort: number };
database: { port: number; containerPort: number };
providerIngress: { port: number; containerPort: number };
restrictedHostAccess?: { bindHost: string; allowedSourceCidrs: string[] };
@@ -247,6 +248,7 @@ export function readConfig(): UniDeskConfig {
publicHost: stringField(network, "publicHost", "network"),
core: portPair(network, "core"),
frontend: portPair(network, "frontend"),
devFrontend: portPair(network, "devFrontend"),
database: portPair(network, "database"),
providerIngress: portPair(network, "providerIngress"),
restrictedHostAccess: optionalRestrictedHostAccess(network),
+26 -59
View File
@@ -23,18 +23,9 @@ async function readJson(url: string, init?: RequestInit): Promise<unknown> {
}
function coreInternalFetch(path: string, init?: { method?: string; body?: unknown }): unknown {
const code = `
const res = await fetch(${JSON.stringify(`http://backend-core:8080${path}`)}, ${JSON.stringify({
method: init?.method ?? "GET",
headers: init?.body === undefined ? undefined : { "content-type": "application/json" },
body: init?.body === undefined ? undefined : JSON.stringify(init.body),
})});
const text = await res.text();
let body = null;
try { body = text ? JSON.parse(text) : null; } catch { body = { text }; }
console.log(JSON.stringify({ ok: res.ok, status: res.status, body }));
`;
const result = runCommand(["docker", "exec", "unidesk-frontend", "bun", "-e", code], repoRoot);
const command = ["docker", "exec", "unidesk-backend-core", "backend-core", "--fetch-json", `http://127.0.0.1:8080${path}`, "--method", init?.method ?? "GET"];
if (init?.body !== undefined) command.push("--body-json", JSON.stringify(init.body));
const result = runCommand(command, repoRoot);
if (result.exitCode !== 0) {
return { ok: false, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) };
}
@@ -46,13 +37,14 @@ function coreInternalFetch(path: string, init?: { method?: string; body?: unknow
}
function coreDockerStatusSummary(): unknown {
const code = `
const res = await fetch('http://backend-core:8080/api/nodes/docker-status');
const text = await res.text();
let body = null;
try { body = text ? JSON.parse(text) : null; } catch { body = { text }; }
const dockerStatuses = (body?.dockerStatuses || []).map((item) => {
const status = item.dockerStatus || {};
const result = runCommand(["docker", "exec", "unidesk-backend-core", "backend-core", "--fetch-json", "http://127.0.0.1:8080/api/nodes/docker-status"], repoRoot);
if (result.exitCode !== 0) {
return { ok: false, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) };
}
try {
const parsed = JSON.parse(result.stdout.trim()) as { ok?: boolean; status?: number; body?: { dockerStatuses?: Array<Record<string, unknown>> } };
const dockerStatuses = (parsed.body?.dockerStatuses || []).map((item) => {
const status = (item.dockerStatus || {}) as Record<string, unknown>;
return {
providerId: item.providerId,
name: item.name,
@@ -64,62 +56,37 @@ function coreDockerStatusSummary(): unknown {
collectedAt: status.collectedAt,
counts: status.counts,
daemon: status.daemon,
containersPreview: (status.containers || []).slice(0, 8).map((container) => ({
id: container.id,
name: container.name,
image: container.image,
state: container.state,
status: container.status,
ports: container.ports,
})),
containersPreview: Array.isArray(status.containers) ? status.containers.slice(0, 8) : [],
},
};
});
console.log(JSON.stringify({ ok: res.ok, status: res.status, body: { ok: body?.ok === true, dockerStatuses } }));
`;
const result = runCommand(["docker", "exec", "unidesk-frontend", "bun", "-e", code], repoRoot);
if (result.exitCode !== 0) {
return { ok: false, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) };
}
try {
return JSON.parse(result.stdout.trim()) as unknown;
return { ok: parsed.ok, status: parsed.status, body: { ok: parsed.body !== undefined, dockerStatuses } };
} catch {
return { ok: true, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) };
}
}
function coreSystemStatusSummary(): unknown {
const code = `
const res = await fetch('http://backend-core:8080/api/nodes/system-status?limit=24');
const text = await res.text();
let body = null;
try { body = text ? JSON.parse(text) : null; } catch { body = { text }; }
const systemStatuses = (body?.systemStatuses || []).map((item) => {
const current = item.current || {};
const result = runCommand(["docker", "exec", "unidesk-backend-core", "backend-core", "--fetch-json", "http://127.0.0.1:8080/api/nodes/system-status?limit=24"], repoRoot);
if (result.exitCode !== 0) {
return { ok: false, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) };
}
try {
const parsed = JSON.parse(result.stdout.trim()) as { ok?: boolean; status?: number; body?: { systemStatuses?: Array<Record<string, unknown>> } };
const systemStatuses = (parsed.body?.systemStatuses || []).map((item) => {
const current = (item.current || {}) as Record<string, unknown>;
const history = Array.isArray(item.history) ? item.history : [];
return {
providerId: item.providerId,
name: item.name,
nodeStatus: item.nodeStatus,
updatedAt: item.updatedAt,
current: item.current ? {
ok: current.ok,
collectedAt: current.collectedAt,
cpu: current.cpu,
memory: current.memory,
disk: current.disk,
} : null,
historyPreview: (item.history || []).slice(-8),
historyCount: (item.history || []).length,
current: item.current ? { ok: current.ok, collectedAt: current.collectedAt, cpu: current.cpu, memory: current.memory, disk: current.disk } : null,
historyPreview: history.slice(-8),
historyCount: history.length,
};
});
console.log(JSON.stringify({ ok: res.ok, status: res.status, body: { ok: body?.ok === true, systemStatuses } }));
`;
const result = runCommand(["docker", "exec", "unidesk-frontend", "bun", "-e", code], repoRoot);
if (result.exitCode !== 0) {
return { ok: false, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) };
}
try {
return JSON.parse(result.stdout.trim()) as unknown;
return { ok: parsed.ok, status: parsed.status, body: { ok: parsed.body !== undefined, systemStatuses } };
} catch {
return { ok: true, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) };
}
+7 -6
View File
@@ -131,8 +131,8 @@ const nativeK3sInstallVersion = "v1.34.1+k3s1";
const nativeK3sImage = "rancher/k3s:v1.34.1-k3s1";
const nativeK3sCtrAddress = "/run/k3s/containerd/containerd.sock";
const unideskRepoUrl = "https://github.com/pikasTech/unidesk";
const d601MaintenanceDeployAllowedServiceIds = new Set<string>();
const devApplySupportedServiceIds = new Set<string>();
const d601MaintenanceDeployAllowedServiceIds = new Set<string>(["backend-core", "frontend"]);
const devApplySupportedServiceIds = new Set<string>(["backend-core", "frontend"]);
const deployEnvironmentTargets: Record<DeployEnvironment, DeployEnvironmentTarget> = {
dev: {
environment: "dev",
@@ -195,7 +195,7 @@ function deployHelp(action: string | undefined = undefined): Record<string, unkn
},
options: [
{ name: "--file <path>", default: defaultDeployFile, description: "Desired-state manifest path relative to the repo root. JSON and ESM JS manifests are supported, for example deploy.json or develop.js." },
{ name: "--env <dev|prod>", description: "Read the named environment from origin/master:deploy.json. Direct D601 service apply is disabled in the current CI-only phase." },
{ name: "--env <dev|prod>", description: "Read the named environment from origin/master:deploy.json. Dev apply is enabled only for backend-core and frontend in D601 unidesk-dev." },
{ name: "--service <id>", description: "Limit reconcile to one service from the manifest." },
{ name: "--dry-run", description: "Prepare and validate without mutating the target service." },
{ name: "--force", description: "Redeploy even when the live commit appears up to date." },
@@ -1005,6 +1005,7 @@ function dockerBuildTimeoutMs(service: UniDeskMicroserviceConfig, options: Deplo
function devK3sPrepullImages(service: UniDeskMicroserviceConfig): string[] {
if (!isDevK3sDeployService(service)) return [];
if (service.id === "backend-core") return ["rust:1-bookworm", "postgres:16-bookworm"];
return ["oven/bun:1-alpine"];
}
@@ -2169,7 +2170,7 @@ async function applyOneService(config: UniDeskConfig, service: UniDeskMicroservi
ok: false,
serviceId: service.id,
skipped: true,
reason: `D601 maintenance-channel direct deployment is disabled for ${service.id}. Current dev automation is CI-only; use ci run-dev-e2e for the temporary namespace smoke runner.`,
reason: `D601 dev deployment is allowed only for backend-core and frontend through the controlled deploy --env dev path; ${service.id} is not enabled. Use ci run-dev-e2e for smoke verification.`,
steps,
};
}
@@ -2344,7 +2345,7 @@ function blockedD601MaintenanceDeployServices(config: UniDeskConfig, manifest: D
}
function d601MaintenanceDeployBlockMessage(blocked: string[]): string {
return `D601 maintenance-channel direct deployment is disabled for direct/managed services in the current CI-only phase; blocked services: ${blocked.join(", ")}. Use ci run-dev-e2e for dev smoke verification.`;
return `D601 dev deployment is enabled only for backend-core and frontend through deploy --env dev; blocked services: ${blocked.join(", ")}. Use ci run-dev-e2e for dev smoke verification.`;
}
async function runApplyNow(config: UniDeskConfig, manifest: DeployManifest, options: DeployOptions): Promise<Record<string, unknown>> {
@@ -2396,7 +2397,7 @@ export async function runDeployCommand(config: UniDeskConfig | null, args: strin
if (options.environment !== "dev") throw new Error("deploy apply --env prod is not enabled yet");
const unsupported = unsupportedDevApplyServices(manifest, options.serviceId);
if (unsupported.length > 0) {
throw new Error(`deploy apply --env dev is disabled for direct service rollout in the current CI-only phase; unsupported selected services: ${unsupported.join(", ")}. Use ci run-dev-e2e for dev smoke verification.`);
throw new Error(`deploy apply --env dev currently supports only backend-core and frontend; unsupported selected services: ${unsupported.join(", ")}. Use ci run-dev-e2e for smoke verification.`);
}
if (config === null) throw new Error("deploy apply --env dev requires config.json");
if (!options.dryRun) {
+19 -31
View File
@@ -20,7 +20,7 @@ export interface ContainerStatus {
ports: string;
}
const rebuildableServices = ["backend-core", "frontend", "provider-gateway", "todo-note", "code-queue-mgr", "project-manager", "baidu-netdisk", "oa-event-flow"] as const;
const rebuildableServices = ["backend-core", "frontend", "dev-frontend-proxy", "provider-gateway", "todo-note", "project-manager", "baidu-netdisk", "oa-event-flow", "code-queue-mgr"] as const;
export type RebuildableService = typeof rebuildableServices[number];
export function isRebuildableService(value: string | undefined): value is RebuildableService {
@@ -28,7 +28,6 @@ export function isRebuildableService(value: string | undefined): value is Rebuil
}
export function resolveComposeCommand(config: UniDeskConfig, envFile: string): string[] {
assertCanonicalServerRoot(config);
const composeFile = rootPath(config.docker.composeFile);
if (commandOk(["docker", "compose", "version"], repoRoot)) {
return ["docker", "compose", "--env-file", envFile, "-f", composeFile, "-p", config.docker.projectName];
@@ -54,21 +53,7 @@ function envValue(value: string): string {
return JSON.stringify(value);
}
function assertCanonicalServerRoot(config: UniDeskConfig): void {
const canonicalRoot = resolve(config.providerGateway.upgrade.hostProjectRoot);
const currentRoot = resolve(repoRoot);
if (currentRoot !== canonicalRoot) {
throw new Error([
"Main-server Docker Compose commands must run from the canonical UniDesk root.",
`canonicalRoot=${canonicalRoot}`,
`currentRoot=${currentRoot}`,
`Run from the canonical root: cd ${canonicalRoot} && bun scripts/cli.ts server <start|status|logs|rebuild|stop>`,
].join(" "));
}
}
export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean): ComposeRuntimeEnv {
assertCanonicalServerRoot(config);
const stateDir = rootPath(config.paths.stateDir);
mkdirSync(stateDir, { recursive: true });
const envFile = join(stateDir, "docker-compose.env");
@@ -107,6 +92,7 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
UNIDESK_PUBLIC_HOST: config.network.publicHost,
UNIDESK_CORE_PORT: String(config.network.core.port),
UNIDESK_FRONTEND_PORT: String(config.network.frontend.port),
UNIDESK_DEV_FRONTEND_PORT: String(config.network.devFrontend.port),
UNIDESK_DATABASE_PORT: String(config.network.database.port),
UNIDESK_DATABASE_BIND_HOST: runtimeSecretWithDefault("UNIDESK_DATABASE_BIND_HOST", restrictedHostBind, "127.0.0.1"),
UNIDESK_PROVIDER_INGRESS_PORT: String(config.network.providerIngress.port),
@@ -141,10 +127,6 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
UNIDESK_LOG_DAY: logDay,
UNIDESK_LOG_PREFIX: logPrefix,
UNIDESK_LOG_RETENTION_BYTES: runtimeSecret("UNIDESK_LOG_RETENTION_BYTES") || "1GiB",
UNIDESK_FRONTEND_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_SERVICE_ID") || "frontend",
UNIDESK_FRONTEND_DEPLOY_REPO: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_REPO"),
UNIDESK_FRONTEND_DEPLOY_COMMIT: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_COMMIT"),
UNIDESK_FRONTEND_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_REQUESTED_COMMIT"),
UNIDESK_HOST_ROOT_SSH_DIR: process.env.UNIDESK_HOST_ROOT_SSH_DIR || "/root/.ssh",
UNIDESK_HOST_SSH_KEY_DIR: config.sshForwarding.keyDir,
UNIDESK_HOST_SSH_HOST: config.sshForwarding.host,
@@ -159,13 +141,16 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
UNIDESK_TODO_NOTE_REMINDER_SCAN_INTERVAL_MS: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_SCAN_INTERVAL_MS") || "30000",
UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_TIMEOUT_MS: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_TIMEOUT_MS") || "15000",
UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_SEND_ATTEMPTS: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_SEND_ATTEMPTS") || "3",
UNIDESK_CODE_QUEUE_MGR_DATABASE_POOL_MAX: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DATABASE_POOL_MAX") || "2",
UNIDESK_CODE_QUEUE_TRACE_DATABASE_POOL_MAX: runtimeSecret("UNIDESK_CODE_QUEUE_TRACE_DATABASE_POOL_MAX") || "1",
UNIDESK_CODE_QUEUE_MINIMAX_API_KEY: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_API_KEY") || runtimeSecret("MINIMAX_API_KEY"),
UNIDESK_CODE_QUEUE_MINIMAX_MODEL: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_MODEL") || runtimeSecret("MINIMAX_MODEL") || "MiniMax-M2.7",
UNIDESK_CODE_QUEUE_MINIMAX_API_BASE: runtimeSecret("UNIDESK_CODE_QUEUE_MINIMAX_API_BASE") || runtimeSecret("MINIMAX_API_BASE") || "https://api.minimaxi.com/v1",
UNIDESK_CODE_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS: runtimeSecretWithDefault("UNIDESK_CODE_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS", "90000", "60000"),
UNIDESK_CODE_QUEUE_REMOTE_WORKDIR: runtimeSecret("UNIDESK_CODE_QUEUE_REMOTE_WORKDIR") || "/home/ubuntu",
UNIDESK_CODE_QUEUE_MAIN_PROVIDER_ID: runtimeSecret("UNIDESK_CODE_QUEUE_MAIN_PROVIDER_ID") || "D601",
UNIDESK_CODE_QUEUE_TRACE_DATABASE_URL: runtimeSecret("UNIDESK_CODE_QUEUE_TRACE_DATABASE_URL")
|| `postgres://${config.database.user}:${config.database.password}@database:${config.network.database.containerPort}/${config.database.name}`,
UNIDESK_CODE_QUEUE_MGR_DATABASE_POOL_MAX: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DATABASE_POOL_MAX") || "2",
UNIDESK_CODE_QUEUE_TRACE_DATABASE_POOL_MAX: runtimeSecret("UNIDESK_CODE_QUEUE_TRACE_DATABASE_POOL_MAX") || "1",
UNIDESK_CODE_QUEUE_EXECUTION_PROVIDER_IDS: runtimeSecret("UNIDESK_CODE_QUEUE_EXECUTION_PROVIDER_IDS") || "D601",
UNIDESK_CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID: runtimeSecret("UNIDESK_CODE_QUEUE_DEV_CONTAINER_DEFAULT_PROVIDER_ID") || "D601",
UNIDESK_CODE_QUEUE_DEV_CONTAINER_IMAGE: runtimeSecret("UNIDESK_CODE_QUEUE_DEV_CONTAINER_IMAGE"),
@@ -250,10 +235,11 @@ export function rebuildService(config: UniDeskConfig, service: RebuildableServic
const watchdogLog = rootPath(".state", "jobs", "compose-rebuild-watchdog.log");
const watchdogInnerScript = [
"set -euo pipefail",
"sleep 20",
`cid=$(${shellJoin(listServiceContainersCommand)} || true)`,
`if [ -z "$cid" ]; then echo "$(date -Is) compose_rebuild_watchdog_restore service=${service}" >> ${shellQuote(watchdogLog)}; ${shellJoin(restoreCommand)} >> ${shellQuote(watchdogLog)} 2>&1 || true; fi`,
].join("\n");
const watchdogScript = `set -euo pipefail; sleep 20; ${shellJoin(["flock", "-w", "300", lockPath, "bash", "-lc", watchdogInnerScript])} || true`;
const watchdogScript = `set -euo pipefail; ${shellJoin(["flock", "-w", "300", lockPath, "bash", "-lc", watchdogInnerScript])} || true`;
const validateScript = [
"ready=0",
"for attempt in $(seq 1 60); do",
@@ -308,6 +294,7 @@ export function rebuildService(config: UniDeskConfig, service: RebuildableServic
function fixedPorts(config: UniDeskConfig): Array<{ name: string; port: number; listening: boolean }> {
return [
{ name: "frontend", port: config.network.frontend.port, listening: isPortListening(config.network.frontend.port) },
{ name: "dev-frontend", port: config.network.devFrontend.port, listening: isPortListening(config.network.devFrontend.port) },
{ name: "provider-ingress", port: config.network.providerIngress.port, listening: isPortListening(config.network.providerIngress.port) },
];
}
@@ -356,9 +343,7 @@ function composeLockedScript(innerScript: string): string {
"set -euo pipefail",
`mkdir -p ${shellQuote(rootPath(".state", "locks"))}`,
`echo ${shellJoin(["compose_lock_wait", lockPath])}`,
// Prevent background helpers spawned by the locked script from inheriting
// the compose lock fd and blocking later server rebuild jobs.
shellJoin(["flock", "-o", lockPath, "bash", "-lc", innerScript]),
shellJoin(["flock", lockPath, "bash", "-lc", innerScript]),
].join("; ");
}
@@ -409,8 +394,8 @@ async function probe(url: string): Promise<unknown> {
}
}
function dockerExecJson(container: string, code: string): unknown {
const result = runCommand(["docker", "exec", container, "bun", "-e", code], repoRoot);
function dockerExecJson(container: string, path: string): unknown {
const result = runCommand(["docker", "exec", container, "backend-core", "--fetch-json", `http://127.0.0.1:8080${path}`], repoRoot);
if (result.exitCode !== 0) {
return { ok: false, exitCode: result.exitCode, stdout: result.stdout.slice(-1200), stderr: result.stderr.slice(-1200) };
}
@@ -433,8 +418,8 @@ export async function stackStatus(config: UniDeskConfig): Promise<unknown> {
const databaseBindHost = runtimeValue("UNIDESK_DATABASE_BIND_HOST") || "127.0.0.1";
const oaEventFlowBindHost = runtimeValue("UNIDESK_OA_EVENT_FLOW_BIND_HOST") || "127.0.0.1";
const oaEventFlowPort = Number(runtimeValue("UNIDESK_OA_EVENT_FLOW_PORT") || "4255");
const coreHealth = dockerExecJson("unidesk-backend-core", "fetch('http://127.0.0.1:8080/health').then(r=>r.json()).then(j=>console.log(JSON.stringify({ok:true,status:200,body:j}))).catch(e=>{console.log(JSON.stringify({ok:false,error:String(e)}));process.exit(1)})");
const overview = dockerExecJson("unidesk-backend-core", "fetch('http://127.0.0.1:8080/api/overview').then(r=>r.json()).then(j=>console.log(JSON.stringify({ok:true,status:200,body:j}))).catch(e=>{console.log(JSON.stringify({ok:false,error:String(e)}));process.exit(1)})");
const coreHealth = dockerExecJson("unidesk-backend-core", "/health");
const overview = dockerExecJson("unidesk-backend-core", "/api/overview");
return {
runtimeEnv,
swap: swapStatus(),
@@ -462,6 +447,7 @@ export async function stackStatus(config: UniDeskConfig): Promise<unknown> {
],
internalPorts: [
{ name: "backend-core", containerPort: config.network.core.containerPort, hostPort: null },
{ name: "dev-frontend-proxy", containerPort: config.network.devFrontend.containerPort, hostPort: config.network.devFrontend.port },
{ name: "database", containerPort: config.network.database.containerPort, hostPort: null },
{ name: "project-manager", containerPort: 4233, hostPort: null },
{ name: "baidu-netdisk", containerPort: 4244, hostPort: null },
@@ -471,12 +457,14 @@ export async function stackStatus(config: UniDeskConfig): Promise<unknown> {
health: {
core: coreHealth,
frontend: await probe(`http://127.0.0.1:${config.network.frontend.port}/health`),
devFrontend: await probe(`http://127.0.0.1:${config.network.devFrontend.port}/health`),
providerIngress: await probe(`http://127.0.0.1:${config.network.providerIngress.port}/health`),
database: dockerExec(config, "unidesk-database", ["pg_isready", "-U", config.database.user, "-d", config.database.name]),
overview,
},
urls: {
frontend: `http://${config.network.publicHost}:${config.network.frontend.port}`,
devFrontend: `http://${config.network.publicHost}:${config.network.devFrontend.port}`,
providerIngress: `ws://${config.network.publicHost}:${config.network.providerIngress.port}/ws/provider`,
internalCore: `http://backend-core:${config.network.core.containerPort}`,
internalDatabase: `postgres://${config.database.user}:***@database:${config.network.database.containerPort}/${config.database.name}`,
@@ -507,7 +495,7 @@ export function stackLogs(config: UniDeskConfig, tailBytes: number): unknown {
const truncated = sizeBytes > tailBytes;
return { path, name: basename(path), sizeBytes, tailBytes, truncated, tail: tailFile(path, tailBytes) };
});
const containerNames = ["unidesk-database", "unidesk-backend-core", "unidesk-frontend", "unidesk-provider-gateway-main", "todo-note-backend", "project-manager-backend", "baidu-netdisk-backend", "oa-event-flow-backend"];
const containerNames = ["unidesk-database", "unidesk-backend-core", "unidesk-frontend", "unidesk-dev-frontend-proxy", "unidesk-provider-gateway-main", "todo-note-backend", "project-manager-backend", "baidu-netdisk-backend", "oa-event-flow-backend"];
const docker = containerNames.map((name) => {
const result = runCommand(["docker", "logs", "--tail", "40", name], repoRoot);
return {
+5 -15
View File
@@ -769,18 +769,9 @@ function runPsql(config: UniDeskConfig, sql: string): { ok: boolean; stdout: str
}
function dockerCoreJson(path: string, init?: { method?: string; body?: unknown }): unknown {
const code = `
const res = await fetch(${JSON.stringify(`http://127.0.0.1:8080${path}`)}, ${JSON.stringify({
method: init?.method ?? "GET",
headers: init?.body === undefined ? undefined : { "content-type": "application/json" },
body: init?.body === undefined ? undefined : JSON.stringify(init.body),
})});
const text = await res.text();
let body = null;
try { body = text ? JSON.parse(text) : null; } catch { body = { text }; }
console.log(JSON.stringify({ ok: res.ok, status: res.status, body }));
`;
const result = runCommand(["docker", "exec", "unidesk-backend-core", "bun", "-e", code], repoRoot);
const command = ["docker", "exec", "unidesk-backend-core", "backend-core", "--fetch-json", `http://127.0.0.1:8080${path}`, "--method", init?.method ?? "GET"];
if (init?.body !== undefined) command.push("--body-json", JSON.stringify(init.body));
const result = runCommand(command, repoRoot);
if (result.exitCode !== 0) return { ok: false, exitCode: result.exitCode, stdout: result.stdout.slice(-1200), stderr: result.stderr.slice(-1200) };
try {
return JSON.parse(result.stdout.trim()) as unknown;
@@ -1872,10 +1863,9 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
await page.waitForSelector('[data-testid="decision-center-record-table"]', { timeout: 30000 });
await page.waitForFunction((title) => {
const text = document.body.innerText;
const lowerText = text.toLowerCase();
return text.includes("Decision Center")
&& text.includes("G0/G1 目标")
&& lowerText.includes("p0/p1 blocker")
&& text.includes("P0/P1 Blocker")
&& text.includes("停放事项")
&& text.includes("最近会议/决议")
&& text.includes("查看原始JSON")
@@ -2942,7 +2932,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
addSelectedCheck(checks, options, "frontend:decision-center-visible",
decisionCenterTextLower.includes("decision center")
&& decisionCenterText.includes("G0/G1 目标")
&& decisionCenterTextLower.includes("p0/p1 blocker")
&& decisionCenterText.includes("P0/P1 Blocker")
&& decisionCenterText.includes("停放事项")
&& decisionCenterText.includes("最近会议/决议")
&& decisionCenterText.includes("全部记录")
+3 -44
View File
@@ -6,50 +6,9 @@ import { jsonByteLength, previewJson } from "./preview";
export function coreInternalFetch(path: string, init?: { method?: string; body?: unknown; maxResponseBytes?: number }): unknown {
if (!path.startsWith("/")) throw new Error("core internal path must start with /");
const maxResponseBytes = Math.max(1024, Math.floor(init?.maxResponseBytes ?? 5_000_000));
const code = `
const res = await fetch(${JSON.stringify(`http://backend-core:8080${path}`)}, ${JSON.stringify({
method: init?.method ?? "GET",
headers: init?.body === undefined ? undefined : { "content-type": "application/json" },
body: init?.body === undefined ? undefined : JSON.stringify(init.body),
})});
const maxResponseBytes = ${JSON.stringify(maxResponseBytes)};
const reader = res.body?.getReader();
const chunks = [];
let bytes = 0;
let responseTruncated = false;
if (reader) {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (bytes + value.byteLength > maxResponseBytes) {
const keep = Math.max(0, maxResponseBytes - bytes);
if (keep > 0) {
chunks.push(value.slice(0, keep));
bytes += keep;
}
responseTruncated = true;
try { await reader.cancel(); } catch {}
break;
}
chunks.push(value);
bytes += value.byteLength;
}
}
const buffer = new Uint8Array(bytes);
let offset = 0;
for (const chunk of chunks) {
buffer.set(chunk, offset);
offset += chunk.byteLength;
}
const text = new TextDecoder().decode(buffer);
let body = null;
try { body = text && !responseTruncated ? JSON.parse(text) : null; } catch { body = { text }; }
if (responseTruncated) {
body = { _unideskResponseTruncated: true, maxResponseBytes, bytesRead: bytes, contentLength: res.headers.get("content-length"), textPreview: text };
}
console.log(JSON.stringify({ ok: res.ok, status: res.status, responseTruncated, responseBytesRead: bytes, responseContentLength: res.headers.get("content-length"), body }));
`;
const result = runCommand(["docker", "exec", "unidesk-frontend", "bun", "-e", code], repoRoot);
const command = ["docker", "exec", "unidesk-backend-core", "backend-core", "--fetch-json", `http://127.0.0.1:8080${path}`, "--method", init?.method ?? "GET", "--max-response-bytes", String(maxResponseBytes)];
if (init?.body !== undefined) command.push("--body-json", JSON.stringify(init.body));
const result = runCommand(command, repoRoot);
if (result.exitCode !== 0) {
return { ok: false, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-1200), stderrTail: result.stderr.slice(-1200) };
}
+3 -7
View File
@@ -805,13 +805,9 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
const child = spawn("docker", [
"exec",
"-i",
"-e",
`PROVIDER_TOKEN=${config.providerGateway.token}`,
"unidesk-frontend",
"bun",
"-e",
brokerSource(),
"--",
"unidesk-backend-core",
"backend-core",
"--ssh-broker",
JSON.stringify(payload),
], {
cwd: repoRoot,