Merge remote-tracking branch 'origin/master' into docs/1755-pr-merge-auto-delivery
This commit is contained in:
@@ -32,6 +32,10 @@ Host workspace、k3s、Windows、GitHub issue/PR route 见 [references/routes.md
|
||||
## P0 边界
|
||||
|
||||
- 远端文本修改优先 `trans <route> apply-patch`;不要 download/upload/sed 拼临时 diff 代替 patch。
|
||||
- `upload` / `download` 仅用于必要的二进制文件或生成物:
|
||||
- 远端文本读取优先使用 `cat` / `rg`;
|
||||
- 远端文本修改优先使用 `apply-patch`;
|
||||
- 成功传输结果的 `hint` 会提示避免对远端文本反复上传下载。
|
||||
- Host/WSL 与 Windows route 的 `apply-patch` 优先走 fs adapter bulk update path;不要为了规避旧的 `read-b64-block` / `write-b64-argv` 慢路径改用临时脚本写文件。
|
||||
- `apply-patch v2` 字节与完整性边界:
|
||||
- localized replacement 必须保留纯 CRLF 和 `legacy-single-byte` 文件的原始字节风格。
|
||||
|
||||
@@ -33,6 +33,168 @@ defaults:
|
||||
targetId: local-7683
|
||||
|
||||
targets:
|
||||
- id: local-7681
|
||||
enabled: true
|
||||
runtime:
|
||||
mode: host-docker
|
||||
workDir: /root/webterm
|
||||
composePath: /root/webterm/build/docker-compose.7681.yaml
|
||||
composeProject: webterm-7681
|
||||
serviceName: web-terminal
|
||||
containerName: web-terminal
|
||||
image: webterm-web-terminal-7681:latest
|
||||
envFile: /root/webterm/.env
|
||||
hostPort: 7681
|
||||
containerPort: 7681
|
||||
privileged: true
|
||||
pid: host
|
||||
sourceMounts:
|
||||
- source: /root/webterm/src
|
||||
target: /app/src
|
||||
readOnly: true
|
||||
- source: /root/webterm/public
|
||||
target: /app/public
|
||||
readOnly: true
|
||||
- source: /root/webterm/scripts/host-shell.sh
|
||||
target: /usr/local/bin/host-shell
|
||||
readOnly: true
|
||||
- source: /root/user_uploads
|
||||
target: /root/user_uploads
|
||||
readOnly: false
|
||||
autoStartSessions:
|
||||
- title: TERM
|
||||
cwd: /root/unidesk
|
||||
command: mycx resume 019f4eeb-bbb3-7e32-bb98-d10753561516
|
||||
cols: 100
|
||||
rows: 30
|
||||
resumePrompt: true
|
||||
fallbackShell: true
|
||||
environment:
|
||||
HOST: 0.0.0.0
|
||||
PORT: "7681"
|
||||
SESSION_COOKIE_NAME: webterm_session_7681
|
||||
SESSION_MAX_AGE_SECONDS: "604800"
|
||||
SERVER_PROVIDER_ID: remote-server
|
||||
SERVER_PROVIDER_NAME: Remote Server
|
||||
TERMINAL_SHELL: /usr/local/bin/host-shell
|
||||
TERMINAL_CWD: /root
|
||||
TERMINAL_REMOTE_CWD: /root/unidesk
|
||||
TERMINAL_START_COMMAND: ""
|
||||
TERMINAL_HISTORY_BYTES: "2097152"
|
||||
TERMINAL_SNAPSHOT_BYTES: "0"
|
||||
TERMINAL_SNAPSHOT_POLICY: tail
|
||||
BROWSER_SNAPSHOT_WRITE_CHUNK_BYTES: "8192"
|
||||
BROWSER_MANUAL_REDRAW_MODE: double
|
||||
BROWSER_MANUAL_REDRAW_DELAY_MS: "60"
|
||||
OUTPUT_BATCH_MS: "8"
|
||||
OUTPUT_BATCH_BYTES: "32768"
|
||||
BROWSER_KEEPALIVE_ENABLED: "true"
|
||||
BROWSER_RESUME_PROBE_ENABLED: "false"
|
||||
BROWSER_KEEPALIVE_INTERVAL_MS: "30000"
|
||||
BROWSER_STALE_TIMEOUT_MS: "180000"
|
||||
BROWSER_SESSION_OUTPUT_IDLE_MS: "3000"
|
||||
BROWSER_OUTPUT_PULSE_MIN_INTERVAL_MS: "250"
|
||||
PROTOCOL_METRICS_ENABLED: "true"
|
||||
PROTOCOL_METRICS_WINDOW: "400"
|
||||
FILE_UPLOAD_DIR: /root/user_uploads
|
||||
FILE_UPLOAD_MAX_BYTES: "104857600"
|
||||
publicExposure:
|
||||
enabled: false
|
||||
publicBaseUrl: https://term.pikapython.com
|
||||
dns:
|
||||
hostname: term.pikapython.com
|
||||
expectedA: 82.156.23.220
|
||||
resolvers: [1.1.1.1, 8.8.8.8, 223.5.5.5, 114.114.114.114]
|
||||
upstream:
|
||||
host: 152.53.229.148
|
||||
port: 7681
|
||||
pk01:
|
||||
route: PK01
|
||||
caddyConfigPath: /etc/caddy/Caddyfile
|
||||
caddyServiceName: caddy
|
||||
responseHeaderTimeoutSeconds: 600
|
||||
|
||||
- id: local-7682
|
||||
enabled: true
|
||||
runtime:
|
||||
mode: host-docker
|
||||
workDir: /root/webterm
|
||||
composePath: /root/webterm/build/docker-compose.7682.yaml
|
||||
composeProject: webterm-7682
|
||||
serviceName: web-terminal
|
||||
containerName: web-terminal-7682
|
||||
image: webterm-web-terminal-7682:latest
|
||||
envFile: /root/webterm/.env
|
||||
hostPort: 7682
|
||||
containerPort: 7681
|
||||
privileged: true
|
||||
pid: host
|
||||
sourceMounts:
|
||||
- source: /root/webterm/src
|
||||
target: /app/src
|
||||
readOnly: true
|
||||
- source: /root/webterm/public
|
||||
target: /app/public
|
||||
readOnly: true
|
||||
- source: /root/webterm/scripts/host-shell.sh
|
||||
target: /usr/local/bin/host-shell
|
||||
readOnly: true
|
||||
- source: /root/user_uploads
|
||||
target: /root/user_uploads
|
||||
readOnly: false
|
||||
autoStartSessions:
|
||||
- title: TERM
|
||||
cwd: /root/unidesk
|
||||
command: mycx resume 019f4eeb-bbb3-7e32-bb98-d10753561516
|
||||
cols: 100
|
||||
rows: 30
|
||||
resumePrompt: true
|
||||
fallbackShell: true
|
||||
environment:
|
||||
HOST: 0.0.0.0
|
||||
PORT: "7681"
|
||||
SESSION_COOKIE_NAME: webterm_session_7682
|
||||
SESSION_MAX_AGE_SECONDS: "604800"
|
||||
SERVER_PROVIDER_ID: remote-server
|
||||
SERVER_PROVIDER_NAME: Remote Server 7682
|
||||
TERMINAL_SHELL: /usr/local/bin/host-shell
|
||||
TERMINAL_CWD: /root
|
||||
TERMINAL_REMOTE_CWD: /root/unidesk
|
||||
TERMINAL_START_COMMAND: ""
|
||||
TERMINAL_HISTORY_BYTES: "2097152"
|
||||
TERMINAL_SNAPSHOT_BYTES: "131072"
|
||||
TERMINAL_SNAPSHOT_POLICY: tail
|
||||
BROWSER_SNAPSHOT_WRITE_CHUNK_BYTES: "8192"
|
||||
BROWSER_MANUAL_REDRAW_MODE: double
|
||||
BROWSER_MANUAL_REDRAW_DELAY_MS: "60"
|
||||
OUTPUT_BATCH_MS: "8"
|
||||
OUTPUT_BATCH_BYTES: "32768"
|
||||
BROWSER_KEEPALIVE_ENABLED: "true"
|
||||
BROWSER_RESUME_PROBE_ENABLED: "false"
|
||||
BROWSER_KEEPALIVE_INTERVAL_MS: "15000"
|
||||
BROWSER_STALE_TIMEOUT_MS: "180000"
|
||||
BROWSER_SESSION_OUTPUT_IDLE_MS: "3000"
|
||||
BROWSER_OUTPUT_PULSE_MIN_INTERVAL_MS: "250"
|
||||
PROTOCOL_METRICS_ENABLED: "true"
|
||||
PROTOCOL_METRICS_WINDOW: "400"
|
||||
FILE_UPLOAD_DIR: /root/user_uploads
|
||||
FILE_UPLOAD_MAX_BYTES: "104857600"
|
||||
publicExposure:
|
||||
enabled: false
|
||||
publicBaseUrl: https://term.pikapython.com
|
||||
dns:
|
||||
hostname: term.pikapython.com
|
||||
expectedA: 82.156.23.220
|
||||
resolvers: [1.1.1.1, 8.8.8.8, 223.5.5.5, 114.114.114.114]
|
||||
upstream:
|
||||
host: 152.53.229.148
|
||||
port: 7682
|
||||
pk01:
|
||||
route: PK01
|
||||
caddyConfigPath: /etc/caddy/Caddyfile
|
||||
caddyServiceName: caddy
|
||||
responseHeaderTimeoutSeconds: 600
|
||||
|
||||
- id: local-7683
|
||||
enabled: true
|
||||
runtime:
|
||||
@@ -68,12 +230,21 @@ targets:
|
||||
cols: 100
|
||||
rows: 30
|
||||
resumePrompt: true
|
||||
fallbackShell: true
|
||||
- title: CONSTAR
|
||||
cwd: /root/unidesk
|
||||
command: mycx resume 019f4b5f-b5ce-76b3-8ccb-508790e9662d
|
||||
cols: 100
|
||||
rows: 30
|
||||
resumePrompt: true
|
||||
fallbackShell: true
|
||||
- title: WORKBENCH
|
||||
cwd: /root/unidesk
|
||||
command: mycx resume 019f4a1a-fd46-7662-8dc2-bf628210739d
|
||||
cols: 100
|
||||
rows: 30
|
||||
resumePrompt: true
|
||||
fallbackShell: true
|
||||
environment:
|
||||
HOST: 0.0.0.0
|
||||
PORT: "7681"
|
||||
@@ -88,7 +259,7 @@ targets:
|
||||
BROWSER_KEEPALIVE_INTERVAL_MS: "30000"
|
||||
BROWSER_STALE_TIMEOUT_MS: "180000"
|
||||
BROWSER_SESSION_OUTPUT_IDLE_MS: "3000"
|
||||
BROWSER_OUTPUT_PULSE_MIN_INTERVAL_MS: "100"
|
||||
BROWSER_OUTPUT_PULSE_MIN_INTERVAL_MS: "250"
|
||||
FILE_UPLOAD_DIR: /root/user_uploads
|
||||
FILE_UPLOAD_MAX_BYTES: "104857600"
|
||||
publicExposure:
|
||||
|
||||
+3
-2
@@ -249,7 +249,7 @@ export function sshHelp(scope: SshHelpScope | undefined = undefined): unknown {
|
||||
"sh and bash helper modes inject a tiny POSIX-compatible printf wrapper before user shell text, so portable printf headings such as `printf \"--- section ---\\n\"` work consistently under dash/sh and bash. Direct argv commands are unchanged.",
|
||||
"For arbitrary stdin streams into a workload command, use a workload route plus `exec --stdin -- <command> ...`; this keeps the route as location-only and avoids heredoc/base64/tar shell wrapping.",
|
||||
"`apply-patch` is the default remote text patch entry and uses the v2 local line-based patch engine with remote read/write operations, including Windows routes such as `D601:win/c/test`, so long Unicode/Chinese lines and pure insertion hunks avoid the legacy remote shell hunk parser. Plain multi-file Update File patches on POSIX host/k3s and Windows workspace routes use bulk read/write operations to avoid per-file SSH round trips. Its stdout follows Codex apply_patch text output rather than UniDesk JSON output; stderr keeps Codex-style failure text and appends one `UNIDESK_APPLY_PATCH_TIMING` JSON summary with durationMs, patchBytes, fileCount, hunkCount, changedCount, remoteOperationCount, remoteOperationCounts and remoteElapsedMs so slow patch runs can be attributed without changing success stdout.",
|
||||
"`upload` and `download` are the default whole-file transfer entries for non-text and generated files. They write through remote temp files, verify byte count and SHA-256 on both sides, and return `verification.automatic=true`, `verification.verified=true`, and `verification.match.{bytes,sha256}=true`; this JSON is the transfer integrity proof, so callers do not need a separate manual `sha256sum` check. Downloads stream over `host.ssh.tcp-pool`, emit progress JSON, and may receive a caller-supplied `--inactivity-timeout-ms` from async artifact/deploy jobs so active large transfers are not killed by the generic short-command budget. Windows route downloads such as `trans D601:win download C:\\Temp\\tool.mjs ./tool.mjs` automatically map drive-letter paths to the same provider's WSL `/mnt/<drive>` host route and still return verified bytes/SHA-256 evidence.",
|
||||
"`upload` 和 `download` 只用于必要的二进制文件或生成物;远端文本应避免反复传输,读取使用 `cat`/`rg`,修改使用 `apply-patch`。成功传输的 JSON 会在 `hint` 中重复该提示,通过远端临时文件写入,自动核对两端字节数和 SHA-256,并返回 `verification.automatic=true`、`verification.verified=true` 和 `verification.match.{bytes,sha256}=true`;该 JSON 已是完整性证据,无需额外执行 `sha256sum`。下载通过 `host.ssh.tcp-pool` 流式传输并输出进度 JSON;受控异步产物任务可传入 `--inactivity-timeout-ms`,只要数据持续流动就不受普通短命令预算影响。Windows route 下载(例如 `trans D601:win download C:\\Temp\\tool.mjs ./tool.mjs`)会自动把盘符路径映射到同一 provider 的 WSL `/mnt/<drive>` host route,同时仍返回字节数和 SHA-256 校验证据。",
|
||||
"`apply-patch-v1` is the only legacy fallback entry: it rejects low-context update hunks by default, reports the matched file:line for each hunk on stderr, and only accepts --allow-loose when the caller has manually reviewed an intentionally ambiguous insertion.",
|
||||
"`sh` inherits provider proxy variables such as HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY and runs target `/bin/sh`; use `bash` only for Bash syntax such as `pipefail`, arrays, substring expansion, or `[[ ... ]]`, not as a proxy workaround.",
|
||||
"Route syntax is `{provider}:{plane}[:{scope...}] {operation} [operation-args...]`: the first argv token locates a distributed target only, and every following token belongs to the operation parser. Host workspace routes use `<provider>:/absolute/workspace`; WSL providers can use `<provider>:win ps` for Windows PowerShell and `<provider>:win cmd` for Windows cmd.exe, with `<provider>:win/c/test ...` mapping the Windows cwd to `C:\\test`; native k3s providers such as D601 and G14 use `<provider>:k3s` for the control plane and `<provider>:k3s:<namespace>:<workload>[:<container>]` for a workload/container. In k3s routes, `:` is the distributed route separator; `/...` is only an in-container filesystem cwd and never selects a container. Prefer operation `--cwd /path` when a container is also specified.",
|
||||
@@ -276,7 +276,7 @@ function sshDownloadHelp(): unknown {
|
||||
command: `${entrypoint} download`,
|
||||
output: "json",
|
||||
scope: "download",
|
||||
description: "Download one remote file through the selected route and verify remote/local bytes plus SHA-256 automatically.",
|
||||
description: "下载一个必要的二进制文件或生成物,并自动核对远端/本地字节数与 SHA-256;远端文本改用 cat/rg/apply-patch。",
|
||||
runtime: {
|
||||
entrypoint,
|
||||
repoRoot,
|
||||
@@ -298,6 +298,7 @@ function sshDownloadHelp(): unknown {
|
||||
"--inactivity-timeout-ms <milliseconds>": "Allow a controlled progress-emitting download to stay open while data continues flowing; ordinary trans operations retain the short runtime budget.",
|
||||
},
|
||||
contract: {
|
||||
hint: "避免对远端文本反复 upload/download;读取优先使用 cat/rg,修改优先使用 apply-patch。",
|
||||
pathOrder: ["remote-file", "local-file"],
|
||||
transport: "host.ssh.tcp-pool",
|
||||
strategy: "tcp-pool-stdout-stream",
|
||||
|
||||
@@ -56,7 +56,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; resumePrompt: boolean }>;
|
||||
autoStartSessions: Array<{ title: string; cwd: string; command: string; cols: number; rows: number; resumePrompt: boolean; fallbackShell: boolean }>;
|
||||
sessionStartModes: SessionStartModes;
|
||||
autoResumePrompt: AutoResumePrompt;
|
||||
environment: Record<string, string>;
|
||||
@@ -115,7 +115,7 @@ export function webtermHelp(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts platform-infra webterm validate",
|
||||
],
|
||||
configPath,
|
||||
description: "Deploy the host-Docker Web Terminal from YAML and expose it through PK01 Caddy HTTPS without touching the existing 7682 service.",
|
||||
description: "Deploy target-scoped host-Docker Web Terminal instances from YAML and optionally expose them through PK01 Caddy HTTPS.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -317,8 +317,6 @@ function parseTarget(record: Record<string, unknown>, path: string, sessionStart
|
||||
},
|
||||
},
|
||||
};
|
||||
if (target.runtime.hostPort === 7682) throw new Error(`${path}.runtime.hostPort must not reuse existing 7682`);
|
||||
if (target.runtime.containerName === "web-terminal-7682") throw new Error(`${path}.runtime.containerName must not reuse web-terminal-7682`);
|
||||
if (!target.publicExposure.publicBaseUrl.startsWith("https://")) throw new Error(`${path}.publicExposure.publicBaseUrl must be https`);
|
||||
return target;
|
||||
}
|
||||
@@ -372,11 +370,14 @@ function resumeCommandWithPrompt(
|
||||
session: WebtermTarget["runtime"]["autoStartSessions"][number],
|
||||
prompt: AutoResumePrompt,
|
||||
): string {
|
||||
if (!session.resumePrompt) return session.command;
|
||||
if (!/^\s*(?:mycx|oncx)\s+resume\s+\S+\s*$/.test(session.command)) {
|
||||
throw new Error(`resumePrompt requires a mycx/oncx resume command: ${session.title}`);
|
||||
let command = session.command.trim();
|
||||
if (session.resumePrompt) {
|
||||
if (!/^\s*(?:mycx|oncx)\s+resume\s+\S+\s*$/.test(session.command)) {
|
||||
throw new Error(`resumePrompt requires a mycx/oncx resume command: ${session.title}`);
|
||||
}
|
||||
command = `${command} ${quoteShellArg(prompt.prompt)}`;
|
||||
}
|
||||
return `${session.command.trim()} ${quoteShellArg(prompt.prompt)}`;
|
||||
return session.fallbackShell ? `${command}; exec /bin/bash -l` : command;
|
||||
}
|
||||
|
||||
function quoteShellArg(value: string): string {
|
||||
@@ -386,11 +387,10 @@ function quoteShellArg(value: string): string {
|
||||
function policyChecks(target: WebtermTarget, compose: string): Array<Record<string, unknown> & { ok: boolean }> {
|
||||
return [
|
||||
{ name: "enabled", ok: target.enabled, detail: "target must be enabled" },
|
||||
{ name: "no-7682-host-port", ok: target.runtime.hostPort !== 7682, detail: "existing 7682 service must not be reused" },
|
||||
{ name: "no-7682-container", ok: target.runtime.containerName !== "web-terminal-7682", detail: "existing 7682 container must not be reused" },
|
||||
{ name: "compose-path-7683", ok: target.runtime.composePath.includes("7683"), detail: target.runtime.composePath },
|
||||
{ name: "compose-path", ok: target.runtime.composePath.endsWith(".yaml"), detail: target.runtime.composePath },
|
||||
{ name: "container-name", ok: compose.includes(`container_name: ${target.runtime.containerName}`), detail: target.runtime.containerName },
|
||||
{ name: "public-https", ok: target.publicExposure.publicBaseUrl.startsWith("https://"), detail: target.publicExposure.publicBaseUrl },
|
||||
{ name: "compose-renders-7683", ok: compose.includes(`"${target.runtime.hostPort}:${target.runtime.containerPort}"`), detail: `${target.runtime.hostPort}:${target.runtime.containerPort}` },
|
||||
{ name: "compose-port", ok: compose.includes(`"${target.runtime.hostPort}:${target.runtime.containerPort}"`), detail: `${target.runtime.hostPort}:${target.runtime.containerPort}` },
|
||||
{ name: "source-mounts-present", ok: target.runtime.sourceMounts.length >= 3, detail: `mounts=${target.runtime.sourceMounts.length}` },
|
||||
];
|
||||
}
|
||||
@@ -557,6 +557,7 @@ function parseAutoStartSessions(value: unknown, path: string): WebtermTarget["ru
|
||||
cols: integerField(record, "cols", itemPath),
|
||||
rows: integerField(record, "rows", itemPath),
|
||||
resumePrompt: booleanField(record, "resumePrompt", itemPath),
|
||||
fallbackShell: booleanField(record, "fallbackShell", itemPath),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,6 +99,15 @@ const fileTransferWriteRawChunkBytes = 131_072;
|
||||
const fileTransferWriteB64ChunkChars = 1_398_104;
|
||||
const fileTransferProgressEveryChunks = 16;
|
||||
|
||||
const fileTransferUsageHint = {
|
||||
code: "prefer-remote-text-operations",
|
||||
message: "避免对远端文本反复 upload/download;读取优先使用 cat/rg,修改优先使用 apply-patch。upload/download 仅用于必要的二进制文件或生成物传输。",
|
||||
preferredOperations: {
|
||||
read: ["cat", "rg"],
|
||||
edit: ["apply-patch"],
|
||||
},
|
||||
};
|
||||
|
||||
export function isSshFileTransferOperation(args: string[]): boolean {
|
||||
const subcommand = args[0] ?? "";
|
||||
return subcommand === "upload" || subcommand === "download";
|
||||
@@ -132,6 +141,7 @@ export async function runSshFileTransferOperation(
|
||||
bytes: expected.bytes,
|
||||
sha256: expected.sha256,
|
||||
verified: true,
|
||||
hint: fileTransferUsageHint,
|
||||
verification,
|
||||
transfer: write,
|
||||
}, null, 2)}\n`);
|
||||
@@ -149,6 +159,7 @@ export async function runSshFileTransferOperation(
|
||||
bytes: download.bytes,
|
||||
sha256: download.sha256,
|
||||
verified: true,
|
||||
hint: fileTransferUsageHint,
|
||||
verification: download.verification,
|
||||
transfer: download.transfer,
|
||||
}, null, 2)}\n`);
|
||||
|
||||
Reference in New Issue
Block a user