merge: 启用 Webterm 会话持久化
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user