chore: checkpoint before performance tuning

This commit is contained in:
Codex
2026-05-11 07:39:37 +00:00
parent a278de032d
commit 5a198baf77
57 changed files with 14768 additions and 1962 deletions
+94 -18
View File
@@ -18,7 +18,7 @@ export interface ContainerStatus {
ports: string;
}
const rebuildableServices = ["backend-core", "frontend", "provider-gateway", "todo-note", "codex-queue"] as const;
const rebuildableServices = ["backend-core", "frontend", "provider-gateway", "todo-note", "codex-queue", "project-manager"] as const;
export type RebuildableService = typeof rebuildableServices[number];
export function isRebuildableService(value: string | undefined): value is RebuildableService {
@@ -112,10 +112,29 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
UNIDESK_HOST_SSH_HOST: config.sshForwarding.host,
UNIDESK_HOST_SSH_PORT: String(config.sshForwarding.port),
UNIDESK_HOST_SSH_USER: config.sshForwarding.user,
UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_ENABLED: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_ENABLED") || "true",
UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_BASE_URL: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_BASE_URL") || "http://backend-core:8080/api/microservices/claudeqq/proxy",
UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_TARGET_TYPE: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_TARGET_TYPE") || "private",
UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_USER_ID: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_USER_ID") || "645275593",
UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_GROUP_ID: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_GROUP_ID"),
UNIDESK_TODO_NOTE_REMINDER_LEAD_MINUTES: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_LEAD_MINUTES") || "10",
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_CODEX_QUEUE_MINIMAX_API_KEY: runtimeSecret("UNIDESK_CODEX_QUEUE_MINIMAX_API_KEY"),
UNIDESK_CODEX_QUEUE_MINIMAX_MODEL: runtimeSecret("UNIDESK_CODEX_QUEUE_MINIMAX_MODEL") || "MiniMax-M2.7",
UNIDESK_CODEX_QUEUE_MINIMAX_API_BASE: runtimeSecret("UNIDESK_CODEX_QUEUE_MINIMAX_API_BASE") || "https://api.minimaxi.com/v1",
UNIDESK_CODEX_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS: runtimeSecret("UNIDESK_CODEX_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS") || "60000",
UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_ENABLED: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_ENABLED") || "true",
UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL") || "http://backend-core:8080/api/microservices/claudeqq/proxy",
UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE") || "private",
UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_USER_ID: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_USER_ID") || "645275593",
UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID"),
UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS") || "12000",
UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS") || "15000",
UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS") || "3",
OPENAI_API_KEY: runtimeSecret("OPENAI_API_KEY"),
CRS_OAI_KEY: runtimeSecret("CRS_OAI_KEY"),
};
writeFileSync(envFile, Object.entries(lines).map(([key, value]) => `${key}=${envValue(value)}`).join("\n") + "\n", "utf8");
return { envFile, logDir, logPrefix };
@@ -136,17 +155,17 @@ export function startStack(config: UniDeskConfig): unknown {
if (occupiedPorts.length > 0 && containers.length === 0) {
throw new Error(`Fixed UniDesk port is occupied before start: ${occupiedPorts.map((p) => `${p.name}:${p.port}`).join(", ")}`);
}
const downCommand = [...compose, "down", "--remove-orphans"];
const upCommand = [...compose, "up", "-d", "--build"];
const command = ["bash", "-lc", `set -euo pipefail; ${shellJoin(downCommand)}; ${shellJoin(upCommand)}`];
const job = startJob("server_start", command, "Build and start UniDesk database, core, frontend, provider gateway, and managed microservice containers");
const upCommand = [...compose, "up", "-d", "--build", "--remove-orphans"];
const command = ["bash", "-lc", composeLockedScript(`set -euo pipefail; ${shellJoin(upCommand)}`)];
const job = startJob("server_start", command, "Build and start UniDesk services without tearing down running queue containers");
return { job, runtimeEnv, command, ports: fixedPorts(config) };
}
export function stopStack(config: UniDeskConfig): unknown {
const runtimeEnv = writeComposeEnv(config, false);
const compose = resolveComposeCommand(config, runtimeEnv.envFile);
const command = [...compose, "down", "--remove-orphans"];
const downCommand = [...compose, "down", "--remove-orphans"];
const command = ["bash", "-lc", composeLockedScript(`set -euo pipefail; ${shellJoin(downCommand)}`)];
const job = startJob("server_stop", command, "Stop all UniDesk Docker services managed by the fixed compose project");
return { job, runtimeEnv, command, portsBeforeStop: fixedPorts(config) };
}
@@ -158,7 +177,7 @@ export function rebuildService(config: UniDeskConfig, service: RebuildableServic
const listServiceContainersCommand = [
"docker",
"ps",
"-aq",
"-q",
"--filter",
`label=com.docker.compose.project=${config.docker.projectName}`,
"--filter",
@@ -166,29 +185,65 @@ export function rebuildService(config: UniDeskConfig, service: RebuildableServic
"--filter",
"label=com.docker.compose.oneoff=False",
];
const upCommand = [...compose, "up", "-d", "--no-deps", service];
const upCommand = [...compose, "up", "-d", "--no-deps", "--force-recreate", service];
const restoreCommand = [...compose, "up", "-d", "--no-deps", service];
const listAllServiceContainersCommand = [...listServiceContainersCommand];
listAllServiceContainersCommand[2] = "-a";
const lockPath = composeLockPath();
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; ${shellJoin(["flock", "-w", "300", lockPath, "bash", "-lc", watchdogInnerScript])} || true`;
const validateScript = [
"ready=0",
"for attempt in $(seq 1 60); do",
`cid=$(${shellJoin(listServiceContainersCommand)} || true)`,
"if [ -n \"$cid\" ]; then",
"health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' $cid 2>/dev/null | head -1 || true)",
`echo "service_container_probe service=${service} attempt=$attempt cid=$cid health=$health"`,
"if [ \"$health\" = \"healthy\" ] || [ \"$health\" = \"running\" ]; then ready=1; break; fi",
"else",
`echo "service_container_probe service=${service} attempt=$attempt cid=missing"`,
"fi",
"sleep 1",
"done",
"if [ \"$ready\" != \"1\" ]; then",
`echo "service_container_not_ready service=${service}" >&2`,
`${shellJoin(listAllServiceContainersCommand)} --format '{{.ID}} {{.Names}} {{.Status}}' >&2 || true`,
"exit 1",
"fi",
].join("\n");
const script = [
"set -euo pipefail",
`echo ${shellJoin(["rebuild_service", service, "build_first_then_replace_container"])}`,
`echo ${shellJoin(["rebuild_service", service, "build_first_then_force_recreate_with_validation"])}`,
shellJoin(buildCommand),
`ids=$(${shellJoin(listServiceContainersCommand)})`,
`if [ -n "$ids" ]; then echo "remove_existing_compose_service_containers service=${service} ids=$ids"; docker rm -f $ids; else echo "no_existing_compose_service_containers service=${service}"; fi`,
`nohup bash -lc ${shellQuote(watchdogScript)} >/dev/null 2>&1 &`,
shellJoin(upCommand),
].join("; ");
const command = ["bash", "-lc", script];
const job = startJob("server_rebuild", command, `Rebuild and replace UniDesk ${service} without Docker Compose v1 recreate`);
validateScript,
].join("\n");
const command = ["bash", "-lc", composeLockedScript(script)];
const jobRunner = service === "codex-queue" ? { runner: "docker" as const, dockerImage: "unidesk-codex-queue:latest" } : {};
const job = startJob("server_rebuild", command, `Rebuild and validate UniDesk ${service} with serialized Docker Compose mutation`, jobRunner);
return {
job,
runtimeEnv,
service,
command,
strategy: {
buildBeforeRemove: true,
removeScope: {
buildBeforeReplace: true,
replaceScope: {
projectLabel: config.docker.projectName,
serviceLabel: service,
},
noDeps: true,
forceRecreate: true,
composeMutationLock: rootPath(".state", "locks", "server-compose.lock"),
jobRunner: service === "codex-queue" ? "docker" : "local",
postUpValidation: true,
namedVolumesPreserved: true,
},
};
@@ -202,7 +257,27 @@ function fixedPorts(config: UniDeskConfig): Array<{ name: string; port: number;
}
function shellJoin(args: string[]): string {
return args.map((arg) => `'${arg.replace(/'/g, `'\\''`)}'`).join(" ");
return args.map(shellQuote).join(" ");
}
function shellQuote(arg: string): string {
return `'${arg.replace(/'/g, `'\\''`)}'`;
}
function composeLockedScript(innerScript: string): string {
const lockPath = composeLockPath();
return [
"set -euo pipefail",
`mkdir -p ${shellQuote(rootPath(".state", "locks"))}`,
`echo ${shellJoin(["compose_lock_wait", lockPath])}`,
shellJoin(["flock", lockPath, "bash", "-lc", innerScript]),
].join("; ");
}
function composeLockPath(): string {
const lockDir = rootPath(".state", "locks");
mkdirSync(lockDir, { recursive: true });
return join(lockDir, "server-compose.lock");
}
function isPortListening(port: number): boolean {
@@ -278,6 +353,7 @@ export async function stackStatus(config: UniDeskConfig): Promise<unknown> {
{ name: "backend-core", containerPort: config.network.core.containerPort, hostPort: null },
{ name: "database", containerPort: config.network.database.containerPort, hostPort: null },
{ name: "codex-queue", containerPort: 4222, hostPort: null },
{ name: "project-manager", containerPort: 4233, hostPort: null },
],
containers: dockerContainers(config),
health: {
@@ -315,7 +391,7 @@ export function stackLogs(config: UniDeskConfig, tailBytes: number): unknown {
const currentFiles = allFiles.filter((path) => basename(path).startsWith(runtimeEnv.logPrefix));
const selectedFiles = (currentFiles.length > 0 ? currentFiles : allFiles.slice(-12)).slice(-12);
const files = selectedFiles.map((path) => ({ path, name: basename(path), tail: tailFile(path, tailBytes) }));
const containerNames = ["unidesk-database", "unidesk-backend-core", "unidesk-frontend", "unidesk-provider-gateway-main", "todo-note-backend", "codex-queue-backend"];
const containerNames = ["unidesk-database", "unidesk-backend-core", "unidesk-frontend", "unidesk-provider-gateway-main", "todo-note-backend", "codex-queue-backend", "project-manager-backend"];
const docker = containerNames.map((name) => {
const result = runCommand(["docker", "logs", "--tail", "40", name], repoRoot);
return { name, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-tailBytes), stderrTail: result.stderr.slice(-tailBytes) };