diff --git a/config/platform-infra/webterm.yaml b/config/platform-infra/webterm.yaml index b1cb181d..1aadc69f 100644 --- a/config/platform-infra/webterm.yaml +++ b/config/platform-infra/webterm.yaml @@ -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" diff --git a/scripts/src/platform-infra-webterm.ts b/scripts/src/platform-infra-webterm.ts index 495e366e..f2374746 100644 --- a/scripts/src/platform-infra-webterm.ts +++ b/scripts/src/platform-infra-webterm.ts @@ -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; }; publicExposure: { @@ -236,15 +254,19 @@ function validate(targetId: string | null): Record { 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, path: string): WebtermTarget { +function parseTarget(record: Record, 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, 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, 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 { 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, 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, 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, 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(value: string, expected: T, path: string): T { if (value !== expected) throw new Error(`${path} must be ${expected}`); return expected;