diff --git a/config/platform-infra/webterm.yaml b/config/platform-infra/webterm.yaml index b33e9651..695bd1ad 100644 --- a/config/platform-infra/webterm.yaml +++ b/config/platform-infra/webterm.yaml @@ -60,6 +60,14 @@ browserPerformance: dashboardSampleWindowSeconds: 60 dashboardRefreshIntervalMs: 1000 +runtimeDatabase: + sourceRef: /root/.unidesk/.state/docker-compose.env + userKey: UNIDESK_DATABASE_USER + passwordKey: UNIDESK_DATABASE_PASSWORD + databaseKey: UNIDESK_DATABASE_NAME + host: host.docker.internal + port: 15432 + defaults: targetId: local-7683 @@ -75,6 +83,7 @@ targets: containerName: web-terminal image: webterm-web-terminal-7681:latest envFile: /root/webterm/.env + runtimeDataDomain: webterm-7681 hostPort: 7681 containerPort: 7681 privileged: true @@ -93,7 +102,8 @@ targets: target: /root/user_uploads readOnly: false autoStartSessions: - - title: TERM + - id: term + title: TERM cwd: /root/unidesk command: mycx resume 019f4eeb-bbb3-7e32-bb98-d10753561516 cols: 100 @@ -154,6 +164,7 @@ targets: containerName: web-terminal-7682 image: webterm-web-terminal-7682:latest envFile: /root/webterm/.env + runtimeDataDomain: webterm-7682 hostPort: 7682 containerPort: 7681 privileged: true @@ -172,7 +183,8 @@ targets: target: /root/user_uploads readOnly: false autoStartSessions: - - title: TERM + - id: term + title: TERM cwd: /root/unidesk command: mycx resume 019f4eeb-bbb3-7e32-bb98-d10753561516 cols: 100 @@ -227,6 +239,7 @@ targets: containerName: web-terminal-7683 image: webterm-web-terminal-7682:latest envFile: /root/webterm/.env + runtimeDataDomain: webterm-7683 hostPort: 7683 containerPort: 7681 privileged: true @@ -254,21 +267,24 @@ targets: uploadBytes: 8388608 maxDurationMs: 10000 autoStartSessions: - - title: 决策中心 + - id: decision-center + title: 决策中心 cwd: /root/unidesk command: mycx resume 019f4937-9820-7b31-9178-344ae93d5a4c cols: 100 rows: 30 resumePrompt: true fallbackShell: true - - title: CONSTAR + - id: constar + title: CONSTAR cwd: /root/unidesk command: mycx resume 019f4b5f-b5ce-76b3-8ccb-508790e9662d cols: 100 rows: 30 resumePrompt: true fallbackShell: true - - title: WORKBENCH + - id: workbench + title: WORKBENCH cwd: /root/unidesk command: mycx resume 019f4a1a-fd46-7662-8dc2-bf628210739d cols: 100 diff --git a/scripts/src/platform-infra-webterm.ts b/scripts/src/platform-infra-webterm.ts index 84d6cc9b..99dd54db 100644 --- a/scripts/src/platform-infra-webterm.ts +++ b/scripts/src/platform-infra-webterm.ts @@ -21,6 +21,7 @@ const serviceName = "webterm"; interface WebtermConfig { defaults: { targetId: string }; + runtimeDatabase: RuntimeDatabase; sessionStartModes: SessionStartModes; autoResumePrompt: AutoResumePrompt; browserTerminalHistory: BrowserTerminalHistory; @@ -29,6 +30,15 @@ interface WebtermConfig { targets: WebtermTarget[]; } +interface RuntimeDatabase { + sourceRef: string; + userKey: string; + passwordKey: string; + databaseKey: string; + host: string; + port: number; +} + interface SessionStartModes { defaultMode: string; options: Array<{ id: string; label: string; command: string }>; @@ -75,13 +85,15 @@ interface WebtermTarget { containerName: string; image: string; envFile: string; + runtimeDataDomain: string; + runtimeDatabase: RuntimeDatabase; hostPort: number; containerPort: number; privileged: boolean; pid: "host" | "container"; sourceMounts: Array<{ source: string; target: string; readOnly: boolean }>; networkSpeedTest: { enabled: boolean; containerName: string; hostPort: number; containerPort: number; publicUrl: string; downloadBytes: number; uploadBytes: number; maxDurationMs: number } | null; - autoStartSessions: Array<{ title: string; cwd: string; command: string; cols: number; rows: number; resumePrompt: boolean; fallbackShell: boolean }>; + autoStartSessions: Array<{ id: string; title: string; cwd: string; command: string; cols: number; rows: number; resumePrompt: boolean; fallbackShell: boolean }>; sessionStartModes: SessionStartModes; autoResumePrompt: AutoResumePrompt; browserTerminalHistory: BrowserTerminalHistory; @@ -216,6 +228,7 @@ async function apply(config: UniDeskConfig, targetId: string | null, confirm: bo const composeResult = spawnSync("docker", ["compose", "-f", target.runtime.composePath, "-p", target.runtime.composeProject, "up", "-d", "--force-recreate"], { cwd: target.runtime.workDir, encoding: "utf8", + env: { ...process.env, ...runtimeDatabaseEnvironment(target.runtime.runtimeDatabase) }, }); const localProbe = providerHealthProbeWithRetry(target.runtime.hostPort, 10, 500); const caddy = target.publicExposure.enabled && target.publicExposure.pk01 !== null @@ -318,10 +331,12 @@ function readWebtermConfig(): WebtermConfig { const browserTerminalHistory = parseBrowserTerminalHistory(recordField(yaml, "browserTerminalHistory", "webterm"), "webterm.browserTerminalHistory"); const browserOutputPolicy = parseBrowserOutputPolicy(recordField(yaml, "browserOutputPolicy", "webterm"), "webterm.browserOutputPolicy"); const browserPerformance = parseBrowserPerformance(recordField(yaml, "browserPerformance", "webterm"), "webterm.browserPerformance"); + const runtimeDatabase = parseRuntimeDatabase(recordField(yaml, "runtimeDatabase", "webterm"), "webterm.runtimeDatabase"); const targetsRaw = yaml.targets; if (!Array.isArray(targetsRaw)) throw new Error("webterm.targets must be an array"); return { defaults: { targetId: stringField(defaults, "targetId", "webterm.defaults") }, + runtimeDatabase, sessionStartModes, autoResumePrompt, browserTerminalHistory, @@ -335,6 +350,7 @@ function readWebtermConfig(): WebtermConfig { browserTerminalHistory, browserOutputPolicy, browserPerformance, + runtimeDatabase, )), }; } @@ -347,6 +363,7 @@ function parseTarget( browserTerminalHistory: BrowserTerminalHistory, browserOutputPolicy: BrowserOutputPolicy, browserPerformance: BrowserPerformance, + runtimeDatabase: RuntimeDatabase, ): WebtermTarget { const runtime = recordField(record, "runtime", path); const exposure = recordField(record, "publicExposure", path); @@ -385,6 +402,8 @@ function parseTarget( containerName: stringField(runtime, "containerName", `${path}.runtime`), image: stringField(runtime, "image", `${path}.runtime`), envFile: stringField(runtime, "envFile", `${path}.runtime`), + runtimeDataDomain: stringField(runtime, "runtimeDataDomain", `${path}.runtime`), + runtimeDatabase, hostPort: portField(runtime, "hostPort", `${path}.runtime`), containerPort: portField(runtime, "containerPort", `${path}.runtime`), privileged: booleanField(runtime, "privileged", `${path}.runtime`), @@ -456,6 +475,8 @@ function resolveTarget(targetId: string | null): WebtermTarget { function renderCompose(target: WebtermTarget): string { const environment = { ...target.runtime.environment, + WEBTERM_DATA_DOMAIN: target.runtime.runtimeDataDomain, + WEBTERM_DATABASE_URL: "${WEBTERM_DATABASE_URL}", AUTO_START_SESSIONS_JSON: JSON.stringify(target.runtime.autoStartSessions.map((session) => ({ ...session, command: resumeCommandWithPrompt(session, target.runtime.autoResumePrompt), @@ -505,6 +526,8 @@ services: - "${target.runtime.hostPort}:${target.runtime.containerPort}" privileged: ${target.runtime.privileged ? "true" : "false"} pid: ${target.runtime.pid} + extra_hosts: + - "host.docker.internal:host-gateway" env_file: - ${target.runtime.envFile} volumes: @@ -590,6 +613,12 @@ function targetSummary(target: WebtermTarget): Record { composeProject: target.runtime.composeProject, containerName: target.runtime.containerName, image: target.runtime.image, + runtimeDataDomain: target.runtime.runtimeDataDomain, + runtimeDatabase: { + sourceRef: target.runtime.runtimeDatabase.sourceRef, + keys: [target.runtime.runtimeDatabase.userKey, target.runtime.runtimeDatabase.passwordKey, target.runtime.runtimeDatabase.databaseKey], + valuesPrinted: false, + }, autoStartSessions: target.runtime.autoStartSessions.map((session) => session.title), sessionStartModes: { defaultMode: target.runtime.sessionStartModes.defaultMode, @@ -745,6 +774,7 @@ function parseAutoStartSessions(value: unknown, path: string): WebtermTarget["ru const itemPath = `${path}[${index}]`; const record = asRecord(item, itemPath); return { + id: stringField(record, "id", itemPath), title: stringField(record, "title", itemPath), cwd: stringField(record, "cwd", itemPath), command: stringField(record, "command", itemPath), @@ -756,6 +786,36 @@ function parseAutoStartSessions(value: unknown, path: string): WebtermTarget["ru }); } +function parseRuntimeDatabase(record: Record, path: string): RuntimeDatabase { + return { + sourceRef: stringField(record, "sourceRef", path), + userKey: stringField(record, "userKey", path), + passwordKey: stringField(record, "passwordKey", path), + databaseKey: stringField(record, "databaseKey", path), + host: stringField(record, "host", path), + port: portField(record, "port", path), + }; +} + +function runtimeDatabaseEnvironment(config: RuntimeDatabase): Record { + const values: Record = {}; + for (const line of readFileSync(config.sourceRef, "utf8").split(/\r?\n/u)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const separator = trimmed.indexOf("="); + if (separator <= 0) continue; + values[trimmed.slice(0, separator)] = trimmed.slice(separator + 1).replace(/^['"]|['"]$/gu, ""); + } + const required = (key: string): string => { + const value = values[key]; + if (!value) throw new Error(`missing ${key} in ${config.sourceRef}`); + return value; + }; + return { + WEBTERM_DATABASE_URL: `postgres://${encodeURIComponent(required(config.userKey))}:${encodeURIComponent(required(config.passwordKey))}@${config.host}:${config.port}/${encodeURIComponent(required(config.databaseKey))}`, + }; +} + function parseSessionStartModes(record: Record, path: string): SessionStartModes { const defaultMode = stringField(record, "defaultMode", path); const optionsRecord = recordField(record, "options", path);