merge: 启用 Webterm 会话持久化
This commit is contained in:
@@ -60,6 +60,14 @@ browserPerformance:
|
|||||||
dashboardSampleWindowSeconds: 60
|
dashboardSampleWindowSeconds: 60
|
||||||
dashboardRefreshIntervalMs: 1000
|
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:
|
defaults:
|
||||||
targetId: local-7683
|
targetId: local-7683
|
||||||
|
|
||||||
@@ -75,6 +83,7 @@ targets:
|
|||||||
containerName: web-terminal
|
containerName: web-terminal
|
||||||
image: webterm-web-terminal-7681:latest
|
image: webterm-web-terminal-7681:latest
|
||||||
envFile: /root/webterm/.env
|
envFile: /root/webterm/.env
|
||||||
|
runtimeDataDomain: webterm-7681
|
||||||
hostPort: 7681
|
hostPort: 7681
|
||||||
containerPort: 7681
|
containerPort: 7681
|
||||||
privileged: true
|
privileged: true
|
||||||
@@ -93,7 +102,8 @@ targets:
|
|||||||
target: /root/user_uploads
|
target: /root/user_uploads
|
||||||
readOnly: false
|
readOnly: false
|
||||||
autoStartSessions:
|
autoStartSessions:
|
||||||
- title: TERM
|
- id: term
|
||||||
|
title: TERM
|
||||||
cwd: /root/unidesk
|
cwd: /root/unidesk
|
||||||
command: mycx resume 019f4eeb-bbb3-7e32-bb98-d10753561516
|
command: mycx resume 019f4eeb-bbb3-7e32-bb98-d10753561516
|
||||||
cols: 100
|
cols: 100
|
||||||
@@ -154,6 +164,7 @@ targets:
|
|||||||
containerName: web-terminal-7682
|
containerName: web-terminal-7682
|
||||||
image: webterm-web-terminal-7682:latest
|
image: webterm-web-terminal-7682:latest
|
||||||
envFile: /root/webterm/.env
|
envFile: /root/webterm/.env
|
||||||
|
runtimeDataDomain: webterm-7682
|
||||||
hostPort: 7682
|
hostPort: 7682
|
||||||
containerPort: 7681
|
containerPort: 7681
|
||||||
privileged: true
|
privileged: true
|
||||||
@@ -172,7 +183,8 @@ targets:
|
|||||||
target: /root/user_uploads
|
target: /root/user_uploads
|
||||||
readOnly: false
|
readOnly: false
|
||||||
autoStartSessions:
|
autoStartSessions:
|
||||||
- title: TERM
|
- id: term
|
||||||
|
title: TERM
|
||||||
cwd: /root/unidesk
|
cwd: /root/unidesk
|
||||||
command: mycx resume 019f4eeb-bbb3-7e32-bb98-d10753561516
|
command: mycx resume 019f4eeb-bbb3-7e32-bb98-d10753561516
|
||||||
cols: 100
|
cols: 100
|
||||||
@@ -227,6 +239,7 @@ targets:
|
|||||||
containerName: web-terminal-7683
|
containerName: web-terminal-7683
|
||||||
image: webterm-web-terminal-7682:latest
|
image: webterm-web-terminal-7682:latest
|
||||||
envFile: /root/webterm/.env
|
envFile: /root/webterm/.env
|
||||||
|
runtimeDataDomain: webterm-7683
|
||||||
hostPort: 7683
|
hostPort: 7683
|
||||||
containerPort: 7681
|
containerPort: 7681
|
||||||
privileged: true
|
privileged: true
|
||||||
@@ -254,21 +267,24 @@ targets:
|
|||||||
uploadBytes: 8388608
|
uploadBytes: 8388608
|
||||||
maxDurationMs: 10000
|
maxDurationMs: 10000
|
||||||
autoStartSessions:
|
autoStartSessions:
|
||||||
- title: 决策中心
|
- id: decision-center
|
||||||
|
title: 决策中心
|
||||||
cwd: /root/unidesk
|
cwd: /root/unidesk
|
||||||
command: mycx resume 019f4937-9820-7b31-9178-344ae93d5a4c
|
command: mycx resume 019f4937-9820-7b31-9178-344ae93d5a4c
|
||||||
cols: 100
|
cols: 100
|
||||||
rows: 30
|
rows: 30
|
||||||
resumePrompt: true
|
resumePrompt: true
|
||||||
fallbackShell: true
|
fallbackShell: true
|
||||||
- title: CONSTAR
|
- id: constar
|
||||||
|
title: CONSTAR
|
||||||
cwd: /root/unidesk
|
cwd: /root/unidesk
|
||||||
command: mycx resume 019f4b5f-b5ce-76b3-8ccb-508790e9662d
|
command: mycx resume 019f4b5f-b5ce-76b3-8ccb-508790e9662d
|
||||||
cols: 100
|
cols: 100
|
||||||
rows: 30
|
rows: 30
|
||||||
resumePrompt: true
|
resumePrompt: true
|
||||||
fallbackShell: true
|
fallbackShell: true
|
||||||
- title: WORKBENCH
|
- id: workbench
|
||||||
|
title: WORKBENCH
|
||||||
cwd: /root/unidesk
|
cwd: /root/unidesk
|
||||||
command: mycx resume 019f4a1a-fd46-7662-8dc2-bf628210739d
|
command: mycx resume 019f4a1a-fd46-7662-8dc2-bf628210739d
|
||||||
cols: 100
|
cols: 100
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const serviceName = "webterm";
|
|||||||
|
|
||||||
interface WebtermConfig {
|
interface WebtermConfig {
|
||||||
defaults: { targetId: string };
|
defaults: { targetId: string };
|
||||||
|
runtimeDatabase: RuntimeDatabase;
|
||||||
sessionStartModes: SessionStartModes;
|
sessionStartModes: SessionStartModes;
|
||||||
autoResumePrompt: AutoResumePrompt;
|
autoResumePrompt: AutoResumePrompt;
|
||||||
browserTerminalHistory: BrowserTerminalHistory;
|
browserTerminalHistory: BrowserTerminalHistory;
|
||||||
@@ -29,6 +30,15 @@ interface WebtermConfig {
|
|||||||
targets: WebtermTarget[];
|
targets: WebtermTarget[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RuntimeDatabase {
|
||||||
|
sourceRef: string;
|
||||||
|
userKey: string;
|
||||||
|
passwordKey: string;
|
||||||
|
databaseKey: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface SessionStartModes {
|
interface SessionStartModes {
|
||||||
defaultMode: string;
|
defaultMode: string;
|
||||||
options: Array<{ id: string; label: string; command: string }>;
|
options: Array<{ id: string; label: string; command: string }>;
|
||||||
@@ -75,13 +85,15 @@ interface WebtermTarget {
|
|||||||
containerName: string;
|
containerName: string;
|
||||||
image: string;
|
image: string;
|
||||||
envFile: string;
|
envFile: string;
|
||||||
|
runtimeDataDomain: string;
|
||||||
|
runtimeDatabase: RuntimeDatabase;
|
||||||
hostPort: number;
|
hostPort: number;
|
||||||
containerPort: number;
|
containerPort: number;
|
||||||
privileged: boolean;
|
privileged: boolean;
|
||||||
pid: "host" | "container";
|
pid: "host" | "container";
|
||||||
sourceMounts: Array<{ source: string; target: string; readOnly: boolean }>;
|
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;
|
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;
|
sessionStartModes: SessionStartModes;
|
||||||
autoResumePrompt: AutoResumePrompt;
|
autoResumePrompt: AutoResumePrompt;
|
||||||
browserTerminalHistory: BrowserTerminalHistory;
|
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"], {
|
const composeResult = spawnSync("docker", ["compose", "-f", target.runtime.composePath, "-p", target.runtime.composeProject, "up", "-d", "--force-recreate"], {
|
||||||
cwd: target.runtime.workDir,
|
cwd: target.runtime.workDir,
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
|
env: { ...process.env, ...runtimeDatabaseEnvironment(target.runtime.runtimeDatabase) },
|
||||||
});
|
});
|
||||||
const localProbe = providerHealthProbeWithRetry(target.runtime.hostPort, 10, 500);
|
const localProbe = providerHealthProbeWithRetry(target.runtime.hostPort, 10, 500);
|
||||||
const caddy = target.publicExposure.enabled && target.publicExposure.pk01 !== null
|
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 browserTerminalHistory = parseBrowserTerminalHistory(recordField(yaml, "browserTerminalHistory", "webterm"), "webterm.browserTerminalHistory");
|
||||||
const browserOutputPolicy = parseBrowserOutputPolicy(recordField(yaml, "browserOutputPolicy", "webterm"), "webterm.browserOutputPolicy");
|
const browserOutputPolicy = parseBrowserOutputPolicy(recordField(yaml, "browserOutputPolicy", "webterm"), "webterm.browserOutputPolicy");
|
||||||
const browserPerformance = parseBrowserPerformance(recordField(yaml, "browserPerformance", "webterm"), "webterm.browserPerformance");
|
const browserPerformance = parseBrowserPerformance(recordField(yaml, "browserPerformance", "webterm"), "webterm.browserPerformance");
|
||||||
|
const runtimeDatabase = parseRuntimeDatabase(recordField(yaml, "runtimeDatabase", "webterm"), "webterm.runtimeDatabase");
|
||||||
const targetsRaw = yaml.targets;
|
const targetsRaw = yaml.targets;
|
||||||
if (!Array.isArray(targetsRaw)) throw new Error("webterm.targets must be an array");
|
if (!Array.isArray(targetsRaw)) throw new Error("webterm.targets must be an array");
|
||||||
return {
|
return {
|
||||||
defaults: { targetId: stringField(defaults, "targetId", "webterm.defaults") },
|
defaults: { targetId: stringField(defaults, "targetId", "webterm.defaults") },
|
||||||
|
runtimeDatabase,
|
||||||
sessionStartModes,
|
sessionStartModes,
|
||||||
autoResumePrompt,
|
autoResumePrompt,
|
||||||
browserTerminalHistory,
|
browserTerminalHistory,
|
||||||
@@ -335,6 +350,7 @@ function readWebtermConfig(): WebtermConfig {
|
|||||||
browserTerminalHistory,
|
browserTerminalHistory,
|
||||||
browserOutputPolicy,
|
browserOutputPolicy,
|
||||||
browserPerformance,
|
browserPerformance,
|
||||||
|
runtimeDatabase,
|
||||||
)),
|
)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -347,6 +363,7 @@ function parseTarget(
|
|||||||
browserTerminalHistory: BrowserTerminalHistory,
|
browserTerminalHistory: BrowserTerminalHistory,
|
||||||
browserOutputPolicy: BrowserOutputPolicy,
|
browserOutputPolicy: BrowserOutputPolicy,
|
||||||
browserPerformance: BrowserPerformance,
|
browserPerformance: BrowserPerformance,
|
||||||
|
runtimeDatabase: RuntimeDatabase,
|
||||||
): WebtermTarget {
|
): WebtermTarget {
|
||||||
const runtime = recordField(record, "runtime", path);
|
const runtime = recordField(record, "runtime", path);
|
||||||
const exposure = recordField(record, "publicExposure", path);
|
const exposure = recordField(record, "publicExposure", path);
|
||||||
@@ -385,6 +402,8 @@ function parseTarget(
|
|||||||
containerName: stringField(runtime, "containerName", `${path}.runtime`),
|
containerName: stringField(runtime, "containerName", `${path}.runtime`),
|
||||||
image: stringField(runtime, "image", `${path}.runtime`),
|
image: stringField(runtime, "image", `${path}.runtime`),
|
||||||
envFile: stringField(runtime, "envFile", `${path}.runtime`),
|
envFile: stringField(runtime, "envFile", `${path}.runtime`),
|
||||||
|
runtimeDataDomain: stringField(runtime, "runtimeDataDomain", `${path}.runtime`),
|
||||||
|
runtimeDatabase,
|
||||||
hostPort: portField(runtime, "hostPort", `${path}.runtime`),
|
hostPort: portField(runtime, "hostPort", `${path}.runtime`),
|
||||||
containerPort: portField(runtime, "containerPort", `${path}.runtime`),
|
containerPort: portField(runtime, "containerPort", `${path}.runtime`),
|
||||||
privileged: booleanField(runtime, "privileged", `${path}.runtime`),
|
privileged: booleanField(runtime, "privileged", `${path}.runtime`),
|
||||||
@@ -456,6 +475,8 @@ function resolveTarget(targetId: string | null): WebtermTarget {
|
|||||||
function renderCompose(target: WebtermTarget): string {
|
function renderCompose(target: WebtermTarget): string {
|
||||||
const environment = {
|
const environment = {
|
||||||
...target.runtime.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) => ({
|
AUTO_START_SESSIONS_JSON: JSON.stringify(target.runtime.autoStartSessions.map((session) => ({
|
||||||
...session,
|
...session,
|
||||||
command: resumeCommandWithPrompt(session, target.runtime.autoResumePrompt),
|
command: resumeCommandWithPrompt(session, target.runtime.autoResumePrompt),
|
||||||
@@ -505,6 +526,8 @@ services:
|
|||||||
- "${target.runtime.hostPort}:${target.runtime.containerPort}"
|
- "${target.runtime.hostPort}:${target.runtime.containerPort}"
|
||||||
privileged: ${target.runtime.privileged ? "true" : "false"}
|
privileged: ${target.runtime.privileged ? "true" : "false"}
|
||||||
pid: ${target.runtime.pid}
|
pid: ${target.runtime.pid}
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
env_file:
|
env_file:
|
||||||
- ${target.runtime.envFile}
|
- ${target.runtime.envFile}
|
||||||
volumes:
|
volumes:
|
||||||
@@ -590,6 +613,12 @@ function targetSummary(target: WebtermTarget): Record<string, unknown> {
|
|||||||
composeProject: target.runtime.composeProject,
|
composeProject: target.runtime.composeProject,
|
||||||
containerName: target.runtime.containerName,
|
containerName: target.runtime.containerName,
|
||||||
image: target.runtime.image,
|
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),
|
autoStartSessions: target.runtime.autoStartSessions.map((session) => session.title),
|
||||||
sessionStartModes: {
|
sessionStartModes: {
|
||||||
defaultMode: target.runtime.sessionStartModes.defaultMode,
|
defaultMode: target.runtime.sessionStartModes.defaultMode,
|
||||||
@@ -745,6 +774,7 @@ function parseAutoStartSessions(value: unknown, path: string): WebtermTarget["ru
|
|||||||
const itemPath = `${path}[${index}]`;
|
const itemPath = `${path}[${index}]`;
|
||||||
const record = asRecord(item, itemPath);
|
const record = asRecord(item, itemPath);
|
||||||
return {
|
return {
|
||||||
|
id: stringField(record, "id", itemPath),
|
||||||
title: stringField(record, "title", itemPath),
|
title: stringField(record, "title", itemPath),
|
||||||
cwd: stringField(record, "cwd", itemPath),
|
cwd: stringField(record, "cwd", itemPath),
|
||||||
command: stringField(record, "command", 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 {
|
function parseSessionStartModes(record: Record<string, unknown>, path: string): SessionStartModes {
|
||||||
const defaultMode = stringField(record, "defaultMode", path);
|
const defaultMode = stringField(record, "defaultMode", path);
|
||||||
const optionsRecord = recordField(record, "options", path);
|
const optionsRecord = recordField(record, "options", path);
|
||||||
|
|||||||
Reference in New Issue
Block a user