From 8374757b1fda879b39261126acf7466cf3c47621 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 11 Jul 2026 04:17:13 +0200 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E9=85=8D=E7=BD=AE=20Webterm=20?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E6=81=A2=E5=A4=8D=E4=BC=9A=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/platform-infra/webterm.yaml | 11 +++++++++++ scripts/src/platform-infra-webterm.ts | 25 ++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/config/platform-infra/webterm.yaml b/config/platform-infra/webterm.yaml index b1cb181d..874b61da 100644 --- a/config/platform-infra/webterm.yaml +++ b/config/platform-infra/webterm.yaml @@ -34,6 +34,17 @@ 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 + - title: 71-FILTER + cwd: /root/unidesk + command: mycx resume 019f4b5f-b5ce-76b3-8ccb-508790e9662d + cols: 100 + rows: 30 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..3893dc7d 100644 --- a/scripts/src/platform-infra-webterm.ts +++ b/scripts/src/platform-infra-webterm.ts @@ -41,6 +41,7 @@ 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 }>; environment: Record; }; publicExposure: { @@ -252,6 +253,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 +271,7 @@ 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, environment, }, publicExposure: { @@ -306,7 +309,11 @@ 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), + }; + const envLines = Object.entries(environment) .map(([key, value]) => ` ${key}: ${quoteYaml(value)}`) .join("\n"); const volumeLines = target.runtime.sourceMounts @@ -378,6 +385,7 @@ 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), sourceMounts: target.runtime.sourceMounts.map((mount) => `${mount.source}->${mount.target}${mount.readOnly ? ":ro" : ""}`), hostPort: target.runtime.hostPort, containerPort: target.runtime.containerPort, @@ -481,6 +489,21 @@ 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), + }; + }); +} + function parseLiteral(value: string, expected: T, path: string): T { if (value !== expected) throw new Error(`${path} must be ${expected}`); return expected; From fdd55ade335912bae50f3bdee8671491cc3b76ab Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 11 Jul 2026 04:38:53 +0200 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E9=85=8D=E7=BD=AE=20Webterm=20?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E5=90=AF=E5=8A=A8=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/platform-infra/webterm.yaml | 15 +++++++++- scripts/src/platform-infra-webterm.ts | 43 +++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/config/platform-infra/webterm.yaml b/config/platform-infra/webterm.yaml index 874b61da..4479a82a 100644 --- a/config/platform-infra/webterm.yaml +++ b/config/platform-infra/webterm.yaml @@ -5,6 +5,19 @@ 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 + defaults: targetId: local-7683 @@ -40,7 +53,7 @@ targets: command: mycx resume 019f4937-9820-7b31-9178-344ae93d5a4c cols: 100 rows: 30 - - title: 71-FILTER + - title: CONSTAR cwd: /root/unidesk command: mycx resume 019f4b5f-b5ce-76b3-8ccb-508790e9662d cols: 100 diff --git a/scripts/src/platform-infra-webterm.ts b/scripts/src/platform-infra-webterm.ts index 3893dc7d..3830cdbc 100644 --- a/scripts/src/platform-infra-webterm.ts +++ b/scripts/src/platform-infra-webterm.ts @@ -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; }; publicExposure: { @@ -237,15 +244,17 @@ 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 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, path: string): WebtermTarget { +function parseTarget(record: Record, 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, 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 { 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, 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 parseLiteral(value: string, expected: T, path: string): T { if (value !== expected) throw new Error(`${path} must be ${expected}`); return expected; From c5bfd97656a170f3bd226ccded41335683c3276d Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 11 Jul 2026 04:59:39 +0200 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E9=85=8D=E7=BD=AE=20Webterm=20?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=81=A2=E5=A4=8D=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/platform-infra/webterm.yaml | 13 ++++++++ scripts/src/platform-infra-webterm.ts | 45 +++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/config/platform-infra/webterm.yaml b/config/platform-infra/webterm.yaml index 4479a82a..1aadc69f 100644 --- a/config/platform-infra/webterm.yaml +++ b/config/platform-infra/webterm.yaml @@ -18,6 +18,17 @@ sessionStartModes: 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 @@ -53,11 +64,13 @@ targets: 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 3830cdbc..f2374746 100644 --- a/scripts/src/platform-infra-webterm.ts +++ b/scripts/src/platform-infra-webterm.ts @@ -22,6 +22,7 @@ const serviceName = "webterm"; interface WebtermConfig { defaults: { targetId: string }; sessionStartModes: SessionStartModes; + autoResumePrompt: AutoResumePrompt; targets: WebtermTarget[]; } @@ -30,6 +31,14 @@ interface SessionStartModes { 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; @@ -47,8 +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 }>; + autoStartSessions: Array<{ title: string; cwd: string; command: string; cols: number; rows: number; resumePrompt: boolean }>; sessionStartModes: SessionStartModes; + autoResumePrompt: AutoResumePrompt; environment: Record; }; publicExposure: { @@ -245,16 +255,18 @@ 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") }, sessionStartModes, - 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, sessionStartModes: SessionStartModes): 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`); @@ -282,6 +294,7 @@ function parseTarget(record: Record, path: string, sessionStart sourceMounts, autoStartSessions, sessionStartModes, + autoResumePrompt, environment, }, publicExposure: { @@ -324,6 +337,7 @@ function renderCompose(target: WebtermTarget): string { 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)}`) @@ -402,6 +416,14 @@ function targetSummary(target: WebtermTarget): Record { 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, @@ -516,6 +538,7 @@ function parseAutoStartSessions(value: unknown, path: string): WebtermTarget["ru command: stringField(record, "command", itemPath), cols: integerField(record, "cols", itemPath), rows: integerField(record, "rows", itemPath), + resumePrompt: booleanField(record, "resumePrompt", itemPath), }; }); } @@ -543,6 +566,22 @@ function commandField(record: Record, path: string): 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;