feat: 配置 Webterm 会话启动模式

This commit is contained in:
Codex
2026-07-11 04:38:53 +02:00
parent 8374757b1f
commit fdd55ade33
2 changed files with 55 additions and 3 deletions
+41 -2
View File
@@ -21,9 +21,15 @@ const serviceName = "webterm";
interface WebtermConfig {
defaults: { targetId: string };
sessionStartModes: SessionStartModes;
targets: WebtermTarget[];
}
interface SessionStartModes {
defaultMode: string;
options: Array<{ id: string; label: string; command: string }>;
}
interface WebtermTarget {
id: string;
enabled: boolean;
@@ -42,6 +48,7 @@ interface WebtermTarget {
pid: "host" | "container";
sourceMounts: Array<{ source: string; target: string; readOnly: boolean }>;
autoStartSessions: Array<{ title: string; cwd: string; command: string; cols: number; rows: number }>;
sessionStartModes: SessionStartModes;
environment: Record<string, string>;
};
publicExposure: {
@@ -237,15 +244,17 @@ function validate(targetId: string | null): Record<string, unknown> {
function readWebtermConfig(): WebtermConfig {
const yaml = readYamlRecord(configPath, "platform-infra-webterm");
const defaults = recordField(yaml, "defaults", "webterm");
const sessionStartModes = parseSessionStartModes(recordField(yaml, "sessionStartModes", "webterm"), "webterm.sessionStartModes");
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") },
targets: targetsRaw.map((item, index) => parseTarget(asRecord(item, `webterm.targets[${index}]`), `webterm.targets[${index}]`)),
sessionStartModes,
targets: targetsRaw.map((item, index) => parseTarget(asRecord(item, `webterm.targets[${index}]`), `webterm.targets[${index}]`, sessionStartModes)),
};
}
function parseTarget(record: Record<string, unknown>, path: string): WebtermTarget {
function parseTarget(record: Record<string, unknown>, path: string, sessionStartModes: SessionStartModes): WebtermTarget {
const runtime = recordField(record, "runtime", path);
const exposure = recordField(record, "publicExposure", path);
const dns = recordField(exposure, "dns", `${path}.publicExposure`);
@@ -272,6 +281,7 @@ function parseTarget(record: Record<string, unknown>, path: string): WebtermTarg
pid: parsePid(stringField(runtime, "pid", `${path}.runtime`), `${path}.runtime.pid`),
sourceMounts,
autoStartSessions,
sessionStartModes,
environment,
},
publicExposure: {
@@ -312,6 +322,8 @@ function renderCompose(target: WebtermTarget): string {
const environment = {
...target.runtime.environment,
AUTO_START_SESSIONS_JSON: JSON.stringify(target.runtime.autoStartSessions),
SESSION_START_MODES_JSON: JSON.stringify(target.runtime.sessionStartModes.options),
SESSION_START_DEFAULT_MODE: target.runtime.sessionStartModes.defaultMode,
};
const envLines = Object.entries(environment)
.map(([key, value]) => ` ${key}: ${quoteYaml(value)}`)
@@ -386,6 +398,10 @@ function targetSummary(target: WebtermTarget): Record<string, unknown> {
containerName: target.runtime.containerName,
image: target.runtime.image,
autoStartSessions: target.runtime.autoStartSessions.map((session) => session.title),
sessionStartModes: {
defaultMode: target.runtime.sessionStartModes.defaultMode,
options: target.runtime.sessionStartModes.options.map((mode) => mode.id),
},
sourceMounts: target.runtime.sourceMounts.map((mount) => `${mount.source}->${mount.target}${mount.readOnly ? ":ro" : ""}`),
hostPort: target.runtime.hostPort,
containerPort: target.runtime.containerPort,
@@ -504,6 +520,29 @@ function parseAutoStartSessions(value: unknown, path: string): WebtermTarget["ru
});
}
function parseSessionStartModes(record: Record<string, unknown>, path: string): SessionStartModes {
const defaultMode = stringField(record, "defaultMode", path);
const optionsRecord = recordField(record, "options", path);
const options = Object.entries(optionsRecord).map(([id, item]) => {
const itemPath = `${path}.options.${id}`;
const option = asRecord(item, itemPath);
return {
id,
label: stringField(option, "label", itemPath),
command: commandField(option, itemPath),
};
});
if (options.length === 0) throw new Error(`${path}.options must be a non-empty record`);
if (!options.some((option) => option.id === defaultMode)) throw new Error(`${path}.defaultMode must reference an option id`);
return { defaultMode, options };
}
function commandField(record: Record<string, unknown>, path: string): string {
const value = record.command;
if (typeof value !== "string") throw new Error(`${path}.command must be a string`);
return value.trim();
}
function parseLiteral<T extends string>(value: string, expected: T, path: string): T {
if (value !== expected) throw new Error(`${path} must be ${expected}`);
return expected;