Merge pull request #257 from pikasTech/fix/d601-host-compose-status

fix: 降级 D601 host compose status 入口
This commit is contained in:
Lyon
2026-06-11 08:00:41 +08:00
committed by GitHub
4 changed files with 237 additions and 24 deletions
+8 -2
View File
@@ -374,7 +374,10 @@ async function main(): Promise<void> {
return;
}
if (sub === "start") {
emitJson(commandName, startStack(config));
const result = startStack(config);
const ok = (result as { ok?: unknown }).ok !== false;
emitJson(commandName, result, ok);
if (!ok) process.exitCode = 1;
return;
}
if (sub === "stop") {
@@ -382,7 +385,10 @@ async function main(): Promise<void> {
return;
}
if (sub === "status") {
emitJson(commandName, await stackStatus(config));
const result = await stackStatus(config);
const ok = (result as { ok?: unknown }).ok !== false;
emitJson(commandName, result, ok);
if (!ok) process.exitCode = 1;
return;
}
if (sub === "swap") {
@@ -0,0 +1,48 @@
import { readConfig } from "./src/config";
import { deprecatedD601HostComposeEntryForTest, type ComposeRuntimeEnv, type ContainerStatus } from "./src/docker";
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
}
const runtimeEnv: ComposeRuntimeEnv = {
envFile: "/home/ubuntu/workspace/unidesk-dev/.state/docker-compose.env",
logDir: "not-created-on-deprecated-d601-host-compose-entry",
logDay: "20260610",
logPrefix: "20260610_120000",
};
const config = readConfig();
const deprecated = deprecatedD601HostComposeEntryForTest(config, runtimeEnv, [], "/home/ubuntu/workspace/unidesk-dev");
assertCondition(deprecated?.ok === false, "D601 host compose entry should be a controlled failure", deprecated);
assertCondition(deprecated.error === "deprecated-host-compose-entry", "D601 host compose entry should expose the deprecation classifier", deprecated);
assertCondition(deprecated.runnerDisposition === "business-failed", "D601 host compose entry should be classified as business-failed", deprecated);
assertCondition(deprecated.decision.hostComposeRetained === false, "D601 host compose entry should be explicitly deprecated", deprecated.decision);
assertCondition(deprecated.correctEntrypoints.d601NativeK3s.includes("trans D601:k3s"), "D601 status should point to native k3s as the D601 acceptance entry", deprecated.correctEntrypoints);
assertCondition(deprecated.correctEntrypoints.mainServerStatus.includes("/root/unidesk"), "D601 status should point main-server compose checks at /root/unidesk", deprecated.correctEntrypoints);
assertCondition(deprecated.evidence.logDir === runtimeEnv.logDir, "D601 status should preserve repo-local runtime evidence", deprecated.evidence);
assertCondition(!JSON.stringify(deprecated).includes("/workspace/unidesk/logs"), "D601 status should not surface the stale /workspace/unidesk/logs permission path", deprecated);
assertCondition(deprecated.evidence.conflictingListeners.every((item) => item.expected === "ignored-on-deprecated-d601-host-compose-entry"), "D601 status should mark occupied public ports as ignored for this deprecated entry", deprecated.evidence.conflictingListeners);
const legacyOperatorPath = deprecatedD601HostComposeEntryForTest(config, runtimeEnv, [], "/home/ubuntu/unidesk");
assertCondition(legacyOperatorPath?.error === "deprecated-host-compose-entry", "legacy D601 operator checkout should receive the same deprecation classifier", legacyOperatorPath);
const staleWorkspacePath = deprecatedD601HostComposeEntryForTest(config, runtimeEnv, [], "/workspace/unidesk");
assertCondition(staleWorkspacePath?.error === "deprecated-host-compose-entry", "stale /workspace D601 checkout should be classified before log directory writes", staleWorkspacePath);
assertCondition(!JSON.stringify(staleWorkspacePath).includes("/workspace/unidesk/logs"), "stale /workspace D601 checkout should not surface the stale log permission path", staleWorkspacePath);
const canonicalMainServer = deprecatedD601HostComposeEntryForTest(config, runtimeEnv, [], "/root/unidesk");
assertCondition(canonicalMainServer === null, "canonical main-server checkout should keep the normal compose path", canonicalMainServer);
const existingContainer: ContainerStatus = {
id: "abc123",
name: "unidesk-backend-core",
image: "unidesk/backend-core:test",
status: "Up",
ports: "",
};
const activeCompose = deprecatedD601HostComposeEntryForTest(config, runtimeEnv, [existingContainer], "/home/ubuntu/workspace/unidesk-dev");
assertCondition(activeCompose === null, "D601 checkout with compose containers should keep the normal compose path", activeCompose);
console.log(JSON.stringify({ ok: true, contract: "d601-host-compose-status" }, null, 2));
+92 -2
View File
@@ -20,6 +20,29 @@ export interface ContainerStatus {
ports: string;
}
export interface DeprecatedHostComposeEntry {
ok: false;
error: "deprecated-host-compose-entry";
runnerDisposition: "business-failed";
hostRole: "d601-provider-host";
composeProject: string;
cwd: string;
message: string;
decision: {
hostComposeRetained: false;
disposition: "deprecated";
reason: string;
};
evidence: {
containers: ContainerStatus[];
publicPorts: Array<{ name: string; port: number; listening: boolean }>;
conflictingListeners: Array<{ name: string; port: number; listening: boolean; expected: string }>;
logDir: string;
};
correctEntrypoints: Record<string, string>;
references: string[];
}
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];
@@ -103,6 +126,16 @@ function envValue(value: string): string {
return JSON.stringify(value);
}
function composeRuntimeEnvPreview(config: UniDeskConfig): ComposeRuntimeEnv {
const parts = localDateParts(new Date());
return {
envFile: join(rootPath(config.paths.stateDir), "docker-compose.env"),
logDir: "not-created-on-deprecated-d601-host-compose-entry",
logDay: parts.day,
logPrefix: parts.stamp,
};
}
export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean): ComposeRuntimeEnv {
const stateDir = rootPath(config.paths.stateDir);
mkdirSync(stateDir, { recursive: true });
@@ -290,9 +323,11 @@ export function composeConfig(config: UniDeskConfig): { runtimeEnv: ComposeRunti
}
export function startStack(config: UniDeskConfig): unknown {
const containers = dockerContainers(config);
const deprecatedEntry = deprecatedD601HostComposeEntry(config, composeRuntimeEnvPreview(config), containers);
if (deprecatedEntry !== null) return deprecatedEntry;
const runtimeEnv = writeComposeEnv(config, true);
const compose = resolveComposeCommand(config, runtimeEnv.envFile);
const containers = dockerContainers(config);
const occupiedPorts = fixedPorts(config).filter((item) => item.listening);
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(", ")}`);
@@ -528,6 +563,9 @@ function dockerExec(config: UniDeskConfig, container: string, command: string[])
}
export async function stackStatus(config: UniDeskConfig): Promise<unknown> {
const containers = dockerContainers(config);
const deprecatedEntry = deprecatedD601HostComposeEntry(config, composeRuntimeEnvPreview(config), containers);
if (deprecatedEntry !== null) return deprecatedEntry;
const runtimeEnv = writeComposeEnv(config, false);
const runtimeRaw = existsSync(runtimeEnv.envFile) ? readFileSync(runtimeEnv.envFile, "utf8") : "";
const runtimeValue = (key: string): string => runtimeRaw.match(new RegExp(`^${key}=(.*)$`, "m"))?.[1]?.replace(/^"|"$/g, "") ?? "";
@@ -569,7 +607,7 @@ export async function stackStatus(config: UniDeskConfig): Promise<unknown> {
{ name: "baidu-netdisk", containerPort: 4244, hostPort: null },
{ name: "oa-event-flow", containerPort: 4255, hostPort: null },
],
containers: dockerContainers(config),
containers,
health: {
core: coreHealth,
frontend: await probe(`http://127.0.0.1:${config.network.frontend.port}/health`),
@@ -590,6 +628,58 @@ export async function stackStatus(config: UniDeskConfig): Promise<unknown> {
};
}
export function deprecatedD601HostComposeEntryForTest(config: UniDeskConfig, runtimeEnv: ComposeRuntimeEnv, containers: ContainerStatus[], currentRoot: string): DeprecatedHostComposeEntry | null {
return deprecatedD601HostComposeEntry(config, runtimeEnv, containers, currentRoot);
}
function deprecatedD601HostComposeEntry(config: UniDeskConfig, runtimeEnv: ComposeRuntimeEnv, containers: ContainerStatus[], currentRoot = repoRoot): DeprecatedHostComposeEntry | null {
const cwd = resolve(currentRoot);
const d601Workspaces = ["/home/ubuntu/unidesk", "/home/ubuntu/workspace/unidesk-dev", "/workspace/unidesk"];
const cwdLooksD601 = d601Workspaces.some((workspace) => cwd === workspace || cwd.startsWith(`${workspace}/.worktree/`));
const upgradeRootLooksMainServer = config.providerGateway.upgrade.hostProjectRoot === "/root/unidesk";
if (!cwdLooksD601 || !upgradeRootLooksMainServer || containers.length > 0) return null;
const publicPorts = fixedPorts(config);
const conflictingListeners = publicPorts
.filter((item) => item.listening)
.map((item) => ({
name: item.name,
port: item.port,
listening: item.listening,
expected: "ignored-on-deprecated-d601-host-compose-entry",
}));
return {
ok: false,
error: "deprecated-host-compose-entry",
runnerDisposition: "business-failed",
hostRole: "d601-provider-host",
composeProject: config.docker.projectName,
cwd,
message: "D601 host Compose is not the UniDesk server acceptance path. This command is deprecated here and does not start or validate the main-server stack.",
decision: {
hostComposeRetained: false,
disposition: "deprecated",
reason: "D601 owns provider-local Docker services and native k3s workloads; main-server Compose remains rooted at /root/unidesk on the main server.",
},
evidence: {
containers,
publicPorts,
conflictingListeners,
logDir: runtimeEnv.logDir,
},
correctEntrypoints: {
mainServerStatus: "run bun scripts/cli.ts server status from /root/unidesk on the main server",
d601NativeK3s: "trans D601:k3s kubectl get deploy,svc,pod,endpoints -n unidesk -o wide",
decisionCenter: "bun scripts/cli.ts microservice health decision-center",
d601DevDeploy: "bun scripts/cli.ts deploy plan --env dev --service <service>",
},
references: [
"docs/reference/deployment.md#docker-compose-runtime",
"docs/reference/dev-environment.md#d601-unidesk-workspace",
"docs/reference/deploy.md#d601-native-k3s-emergency-guard",
],
};
}
function listLogFiles(root: string): string[] {
if (!existsSync(root)) return [];
const entries = readdirSync(root, { withFileTypes: true });
+89 -20
View File
@@ -595,6 +595,90 @@ function artifactRegistryHelp(): unknown {
};
}
function agentRunHelpSummary(): unknown {
return {
command: "agentrun v01 aipod-specs|queue|sessions|control-plane|git-mirror",
output: "json",
usage: [
"bun scripts/cli.ts agentrun v01 aipod-specs show Artificer",
"bun scripts/cli.ts agentrun v01 queue commander --reader-id cli",
"bun scripts/cli.ts agentrun v01 sessions trace <sessionId> --after-seq 0 --limit 100",
"bun scripts/cli.ts agentrun v01 control-plane status",
],
description: "Operate AgentRun v0.1 AipodSpec, queue, sessions, and G14 control-plane entrypoints.",
};
}
function platformInfraHelpSummary(): unknown {
return {
command: "platform-infra sub2api plan|apply|status|validate|codex-pool",
output: "json",
usage: [
"bun scripts/cli.ts platform-infra sub2api plan",
"bun scripts/cli.ts platform-infra sub2api status [--full|--raw]",
"bun scripts/cli.ts platform-infra sub2api codex-pool validate",
],
description: "Operate G14 platform-infra services such as Sub2API and the YAML-controlled Codex pool.",
};
}
function hwlabNodeHelpSummary(): unknown {
return {
command: "hwlab nodes control-plane|git-mirror|secret --node <node> --lane <lane>",
output: "json",
usage: [
"bun scripts/cli.ts hwlab nodes control-plane status --node G14 --lane v03",
"bun scripts/cli.ts hwlab nodes git-mirror status --node G14 --lane v03",
"bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name <secret>",
],
description: "Operate HWLAB node/lane runtime prerequisites with node and lane passed as data.",
};
}
function hwlabG14HelpSummary(): unknown {
return {
command: "hwlab g14 monitor-prs|control-plane|git-mirror|tools-image|retirement",
output: "json",
usage: [
"bun scripts/cli.ts hwlab g14 control-plane status --lane v02",
"bun scripts/cli.ts hwlab g14 trigger-current --lane v02 --dry-run",
"bun scripts/cli.ts hwlab g14 monitor-prs --status",
],
description: "Operate the G14 HWLAB runtime lane control-plane and legacy retirement helpers.",
};
}
function hwlabHelpSummary(): unknown {
return {
command: "hwlab g14|nodes|cd",
output: "json",
usage: [
"bun scripts/cli.ts hwlab g14 control-plane status --lane v02",
"bun scripts/cli.ts hwlab nodes control-plane status --node G14 --lane v03",
"bun scripts/cli.ts hwlab cd audit --env dev",
],
description: "HWLAB operations. Current runtime work uses G14 lane commands; D601 cd is legacy diagnostics only.",
};
}
function helpFallback(help: unknown, error: unknown): unknown {
if (typeof help !== "object" || help === null || Array.isArray(help)) return help;
return {
...help,
degraded: true,
degradedReason: "help-module-load-failed",
error: error instanceof Error ? error.message : String(error),
};
}
async function loadHelp(loader: () => Promise<unknown>, fallback: unknown): Promise<unknown> {
try {
return await loader();
} catch (error) {
return helpFallback(fallback, error);
}
}
export async function staticNamespaceHelp(args: string[]): Promise<unknown | null> {
const [top, sub] = args;
if (!args.slice(1).some(isHelpToken)) return null;
@@ -614,25 +698,10 @@ export async function staticNamespaceHelp(args: string[]): Promise<unknown | nul
if (top === "artifact-registry") return artifactRegistryHelp();
if (top === "auth-broker") return authBrokerHelp();
if (top === "gh") return ghHelp();
if (top === "agentrun") {
const { agentRunHelp } = await import("./agentrun");
return agentRunHelp();
}
if (top === "platform-infra") {
const { platformInfraHelp } = await import("./platform-infra");
return platformInfraHelp();
}
if (top === "hwlab" && (sub === "node" || sub === "nodes")) {
const { hwlabNodeHelp } = await import("./hwlab-node");
return hwlabNodeHelp();
}
if (top === "hwlab" && sub === "g14") {
const { hwlabG14Help } = await import("./hwlab-g14");
return hwlabG14Help();
}
if (top === "hwlab") {
const { hwlabHelp } = await import("./hwlab-cd");
return hwlabHelp();
}
if (top === "agentrun") return loadHelp(async () => (await import("./agentrun")).agentRunHelp(), agentRunHelpSummary());
if (top === "platform-infra") return loadHelp(async () => (await import("./platform-infra")).platformInfraHelp(), platformInfraHelpSummary());
if (top === "hwlab" && (sub === "node" || sub === "nodes")) return loadHelp(async () => (await import("./hwlab-node")).hwlabNodeHelp(), hwlabNodeHelpSummary());
if (top === "hwlab" && sub === "g14") return loadHelp(async () => (await import("./hwlab-g14")).hwlabG14Help(), hwlabG14HelpSummary());
if (top === "hwlab") return loadHelp(async () => (await import("./hwlab-cd")).hwlabHelp(), hwlabHelpSummary());
return null;
}