fix: 启用 Webterm 运行时会话持久化

This commit is contained in:
pikastech
2026-07-21 15:12:09 +02:00
parent c30f3286d8
commit dd0db84157
2 changed files with 82 additions and 6 deletions
+21 -5
View File
@@ -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
+61 -1
View File
@@ -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<string, unknown> {
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<string, unknown>, 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<string, string> {
const values: Record<string, string> = {};
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<string, unknown>, path: string): SessionStartModes {
const defaultMode = stringField(record, "defaultMode", path);
const optionsRecord = recordField(record, "options", path);