merge: 集成 Webterm 自动恢复会话
This commit is contained in:
@@ -5,6 +5,30 @@ metadata:
|
||||
id: webterm-public
|
||||
owner: unidesk
|
||||
|
||||
sessionStartModes:
|
||||
defaultMode: mycx
|
||||
options:
|
||||
bash:
|
||||
label: Bash
|
||||
command: exec bash -l
|
||||
mycx:
|
||||
label: mycx
|
||||
command: mycx
|
||||
oncx:
|
||||
label: oncx
|
||||
command: oncx
|
||||
|
||||
autoResumePrompt:
|
||||
enabled: true
|
||||
prompt: "[自动提醒]如果有未完成任务则继续,没有则直接结束"
|
||||
sendEnter: true
|
||||
enterDelayMs: 500
|
||||
readiness:
|
||||
minDelayMs: 2000
|
||||
minOutputBytes: 64
|
||||
quietPeriodMs: 1500
|
||||
timeoutMs: 60000
|
||||
|
||||
defaults:
|
||||
targetId: local-7683
|
||||
|
||||
@@ -34,6 +58,19 @@ targets:
|
||||
- source: /root/webterm/scripts/host-shell.sh
|
||||
target: /usr/local/bin/host-shell
|
||||
readOnly: true
|
||||
autoStartSessions:
|
||||
- title: 决策中心
|
||||
cwd: /root/unidesk
|
||||
command: mycx resume 019f4937-9820-7b31-9178-344ae93d5a4c
|
||||
cols: 100
|
||||
rows: 30
|
||||
resumePrompt: true
|
||||
- title: CONSTAR
|
||||
cwd: /root/unidesk
|
||||
command: mycx resume 019f4b5f-b5ce-76b3-8ccb-508790e9662d
|
||||
cols: 100
|
||||
rows: 30
|
||||
resumePrompt: true
|
||||
environment:
|
||||
HOST: 0.0.0.0
|
||||
PORT: "7681"
|
||||
|
||||
@@ -21,9 +21,24 @@ const serviceName = "webterm";
|
||||
|
||||
interface WebtermConfig {
|
||||
defaults: { targetId: string };
|
||||
sessionStartModes: SessionStartModes;
|
||||
autoResumePrompt: AutoResumePrompt;
|
||||
targets: WebtermTarget[];
|
||||
}
|
||||
|
||||
interface SessionStartModes {
|
||||
defaultMode: string;
|
||||
options: Array<{ id: string; label: string; command: string }>;
|
||||
}
|
||||
|
||||
interface AutoResumePrompt {
|
||||
enabled: boolean;
|
||||
prompt: string;
|
||||
sendEnter: boolean;
|
||||
enterDelayMs: number;
|
||||
readiness: { minDelayMs: number; minOutputBytes: number; quietPeriodMs: number; timeoutMs: number };
|
||||
}
|
||||
|
||||
interface WebtermTarget {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
@@ -41,6 +56,9 @@ interface WebtermTarget {
|
||||
privileged: boolean;
|
||||
pid: "host" | "container";
|
||||
sourceMounts: Array<{ source: string; target: string; readOnly: boolean }>;
|
||||
autoStartSessions: Array<{ title: string; cwd: string; command: string; cols: number; rows: number; resumePrompt: boolean }>;
|
||||
sessionStartModes: SessionStartModes;
|
||||
autoResumePrompt: AutoResumePrompt;
|
||||
environment: Record<string, string>;
|
||||
};
|
||||
publicExposure: {
|
||||
@@ -236,15 +254,19 @@ 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 autoResumePrompt = parseAutoResumePrompt(recordField(yaml, "autoResumePrompt", "webterm"), "webterm.autoResumePrompt");
|
||||
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,
|
||||
autoResumePrompt,
|
||||
targets: targetsRaw.map((item, index) => parseTarget(asRecord(item, `webterm.targets[${index}]`), `webterm.targets[${index}]`, sessionStartModes, autoResumePrompt)),
|
||||
};
|
||||
}
|
||||
|
||||
function parseTarget(record: Record<string, unknown>, path: string): WebtermTarget {
|
||||
function parseTarget(record: Record<string, unknown>, path: string, sessionStartModes: SessionStartModes, autoResumePrompt: AutoResumePrompt): WebtermTarget {
|
||||
const runtime = recordField(record, "runtime", path);
|
||||
const exposure = recordField(record, "publicExposure", path);
|
||||
const dns = recordField(exposure, "dns", `${path}.publicExposure`);
|
||||
@@ -252,6 +274,7 @@ function parseTarget(record: Record<string, unknown>, path: string): WebtermTarg
|
||||
const pk01 = recordField(exposure, "pk01", `${path}.publicExposure`);
|
||||
const environment = parseEnvironment(recordField(runtime, "environment", `${path}.runtime`), `${path}.runtime.environment`);
|
||||
const sourceMounts = parseSourceMounts(runtime.sourceMounts, `${path}.runtime.sourceMounts`);
|
||||
const autoStartSessions = parseAutoStartSessions(runtime.autoStartSessions, `${path}.runtime.autoStartSessions`);
|
||||
const target: WebtermTarget = {
|
||||
id: stringField(record, "id", path),
|
||||
enabled: booleanField(record, "enabled", path),
|
||||
@@ -269,6 +292,9 @@ function parseTarget(record: Record<string, unknown>, path: string): WebtermTarg
|
||||
privileged: booleanField(runtime, "privileged", `${path}.runtime`),
|
||||
pid: parsePid(stringField(runtime, "pid", `${path}.runtime`), `${path}.runtime.pid`),
|
||||
sourceMounts,
|
||||
autoStartSessions,
|
||||
sessionStartModes,
|
||||
autoResumePrompt,
|
||||
environment,
|
||||
},
|
||||
publicExposure: {
|
||||
@@ -306,7 +332,14 @@ function resolveTarget(targetId: string | null): WebtermTarget {
|
||||
}
|
||||
|
||||
function renderCompose(target: WebtermTarget): string {
|
||||
const envLines = Object.entries(target.runtime.environment)
|
||||
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,
|
||||
AUTO_RESUME_PROMPT_JSON: JSON.stringify(target.runtime.autoResumePrompt),
|
||||
};
|
||||
const envLines = Object.entries(environment)
|
||||
.map(([key, value]) => ` ${key}: ${quoteYaml(value)}`)
|
||||
.join("\n");
|
||||
const volumeLines = target.runtime.sourceMounts
|
||||
@@ -378,6 +411,19 @@ function targetSummary(target: WebtermTarget): Record<string, unknown> {
|
||||
composeProject: target.runtime.composeProject,
|
||||
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),
|
||||
},
|
||||
autoResumePrompt: {
|
||||
enabled: target.runtime.autoResumePrompt.enabled,
|
||||
taggedSessions: target.runtime.autoStartSessions.filter((session) => session.resumePrompt).map((session) => session.title),
|
||||
readiness: target.runtime.autoResumePrompt.readiness,
|
||||
promptBytes: Buffer.byteLength(target.runtime.autoResumePrompt.prompt),
|
||||
sendEnter: target.runtime.autoResumePrompt.sendEnter,
|
||||
enterDelayMs: target.runtime.autoResumePrompt.enterDelayMs,
|
||||
},
|
||||
sourceMounts: target.runtime.sourceMounts.map((mount) => `${mount.source}->${mount.target}${mount.readOnly ? ":ro" : ""}`),
|
||||
hostPort: target.runtime.hostPort,
|
||||
containerPort: target.runtime.containerPort,
|
||||
@@ -481,6 +527,61 @@ function parseSourceMounts(value: unknown, path: string): WebtermTarget["runtime
|
||||
});
|
||||
}
|
||||
|
||||
function parseAutoStartSessions(value: unknown, path: string): WebtermTarget["runtime"]["autoStartSessions"] {
|
||||
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
|
||||
return value.map((item, index) => {
|
||||
const itemPath = `${path}[${index}]`;
|
||||
const record = asRecord(item, itemPath);
|
||||
return {
|
||||
title: stringField(record, "title", itemPath),
|
||||
cwd: stringField(record, "cwd", itemPath),
|
||||
command: stringField(record, "command", itemPath),
|
||||
cols: integerField(record, "cols", itemPath),
|
||||
rows: integerField(record, "rows", itemPath),
|
||||
resumePrompt: booleanField(record, "resumePrompt", itemPath),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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 parseAutoResumePrompt(record: Record<string, unknown>, path: string): AutoResumePrompt {
|
||||
const readiness = recordField(record, "readiness", path);
|
||||
return {
|
||||
enabled: booleanField(record, "enabled", path),
|
||||
prompt: stringField(record, "prompt", path),
|
||||
sendEnter: booleanField(record, "sendEnter", path),
|
||||
enterDelayMs: integerField(record, "enterDelayMs", path),
|
||||
readiness: {
|
||||
minDelayMs: integerField(readiness, "minDelayMs", `${path}.readiness`),
|
||||
minOutputBytes: integerField(readiness, "minOutputBytes", `${path}.readiness`),
|
||||
quietPeriodMs: integerField(readiness, "quietPeriodMs", `${path}.readiness`),
|
||||
timeoutMs: integerField(readiness, "timeoutMs", `${path}.readiness`),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function parseLiteral<T extends string>(value: string, expected: T, path: string): T {
|
||||
if (value !== expected) throw new Error(`${path} must be ${expected}`);
|
||||
return expected;
|
||||
|
||||
Reference in New Issue
Block a user