推进 OPFS G-code 程序路径 helper

This commit is contained in:
2026-06-14 15:57:35 +08:00
parent a158f66071
commit d40a6422e8
5 changed files with 115 additions and 13 deletions

View File

@@ -732,3 +732,64 @@ host_wasm_opfs_browser_smokes=ok
`linuxcnc/gcode/<filename>` 到 filename 的转换下沉为 OPFS G-code store 的可复用 helper
例如 `gcodeFilenameFromProgramPath(...)`,并在 Node OPFS smoke 与 browser smoke 中覆盖
合法 path、非 G-code path fallback 或 reject 行为。仍不解析或改写 G-code 语义。
九、2026-06-14 继续执行记录OPFS G-code program path helper
本轮继续沿 OPFS/session/UI boundary 做小步实质推进,把 G-code OPFS path 到 filename
的转换下沉到 OPFS G-code store避免该 path 规则散落在 UI 层。
完成内容:
- 在 `wasm-port/runtime/opfs/machine-file-store.js` 新增
`gcodeFilenameFromProgramPath(path)`
- 该 helper 使用既有 `splitOpfsPath(...)` 校验 OPFS path且只接受
`linuxcnc/gcode/<filename>` 这种单文件 G-code program path
- 非 G-code program path 或嵌套 G-code path 会 reject避免 browser/UI 调用方静默接受
错误 session 文件清单;
- `wasm-port/runtime/ui/ini-panel/app.js` 的 `Run G-code` 选择逻辑改为复用该 helper
- UI 仍保持安全 fallbacksnapshot 无 G-code path 或 G-code path 非法时回退默认
`GCODE_FILENAME`,不解释或改写 G-code 内容;
- Node OPFS smoke 覆盖合法 G-code OPFS path、非 G-code path reject、嵌套 G-code path
reject
- browser INI panel smoke 覆盖合法 G-code OPFS path 转 filename 和非法 OPFS path reject
- 修正了 snapshot G-code filename 与默认 filename 相同时的来源判定helper 成功解析即
`Program source: snapshot`
- 未改变 G-code 文本内容、OPFS file-service、interpreter run API、canonical-event 解析、
path model 基础语义或 LinuxCNC-owned runtime semantics。
验证已通过:
```bash
git diff --check
wasm-port/tests/opfs/node/verify_file_service.sh
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_ini_panel_browser.sh
wasm-port/tools/verify_vendor_sync.sh
wasm-port/tools/verify_no_standalone_cnc_semantics.sh
SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_interp_wasm.sh
SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_sim_configs_inventory_wasm.sh
SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_interp_browser.sh
wasm-port/tests/host/verify_host_smokes.sh
```
关键输出:
```text
opfs_file_service_node_smoke=ok
browser_ini_opfs_smoke=ok
vendor sync up to date
standalone CNC semantics guard complete
interp_wasm_node_smoke=ok
sim_configs_wasm_node_inventory_executed=28
sim_configs_wasm_node_inventory_passed=28
sim_configs_wasm_node_inventory_skipped=131
sim_configs_wasm_node_inventory_unexpected_fail=0
browser_interp_smoke=ok
host_wasm_opfs_browser_smokes=ok
```
下一步建议:
继续沿 OPFS/session/UI boundary 推进。优先把 UI 的 session snapshot 常量
`SESSION_SNAPSHOT_ID`、`SESSION_SNAPSHOT_FILENAME` 与 G-code fallback metadata 整理成
一个小的 UI session descriptor/helper减少按钮 handler 间重复拼接 snapshot label、
G-code OPFS path 和默认 program source 的逻辑。仍不解析或改写任何 CNC 语义。

View File

@@ -3,9 +3,12 @@ import {
gcodeProgramPath,
machineIniPath,
parameterFilePath,
splitOpfsPath,
toolTablePath,
} from "./path-model.js";
const GCODE_PROGRAM_ROOT = ["linuxcnc", "gcode"];
const MACHINE_FILE_PATHS = {
ini: machineIniPath,
toolTable: toolTablePath,
@@ -43,6 +46,18 @@ export async function loadMachineTextFiles(machineId, options = {}) {
};
}
export function gcodeFilenameFromProgramPath(path) {
const parts = splitOpfsPath(path);
if (
parts.length !== 3 ||
parts[0] !== GCODE_PROGRAM_ROOT[0] ||
parts[1] !== GCODE_PROGRAM_ROOT[1]
) {
throw new Error(`Invalid G-code program OPFS path: ${path}`);
}
return parts[2];
}
export async function saveGcodeProgram(filename, text, options = {}) {
const path = gcodeProgramPath(filename);
await saveTextFile(path, text, options.storage);

View File

@@ -8,6 +8,7 @@ import {
saveTextFile,
} from "../../opfs/file-service.js";
import {
gcodeFilenameFromProgramPath,
loadGcodeProgram,
loadMachineTextFiles,
machineFilePaths,
@@ -311,23 +312,24 @@ function syncEditorToWasmFs() {
requireIniSdk().writeTextFile(WASM_FILE, editor.value);
}
function gcodeFilenameFromSnapshotPath(path) {
const prefix = "linuxcnc/gcode/";
if (typeof path !== "string" || !path.startsWith(prefix)) {
return GCODE_FILENAME;
}
const filename = path.slice(prefix.length);
return filename || GCODE_FILENAME;
}
async function loadSelectedGcodeProgram() {
const opfsPath = restoredSessionSnapshot?.payload?.files?.gcode;
const filename = gcodeFilenameFromSnapshotPath(opfsPath);
const hasSnapshotGcode = typeof opfsPath === "string" && opfsPath.startsWith("linuxcnc/gcode/");
let filename = GCODE_FILENAME;
let selectedOpfsPath = `linuxcnc/gcode/${GCODE_FILENAME}`;
let source = "default";
if (typeof opfsPath === "string") {
try {
filename = gcodeFilenameFromProgramPath(opfsPath);
selectedOpfsPath = opfsPath;
source = "snapshot";
} catch {
filename = GCODE_FILENAME;
}
}
return {
filename,
opfsPath: opfsPath ?? `linuxcnc/gcode/${filename}`,
source: hasSnapshotGcode ? "snapshot" : "default",
opfsPath: selectedOpfsPath,
source,
wasmPath: GCODE_WASM_FILE,
text: await loadGcodeProgram(filename),
};

View File

@@ -22,6 +22,7 @@
saveSessionSnapshot,
} from "../../runtime/opfs/snapshot-store.js";
import {
gcodeFilenameFromProgramPath,
loadGcodeProgram,
loadMachineTextFiles,
saveGcodeProgram,
@@ -328,6 +329,16 @@ TOOL_TABLE = browser-tool.tbl
"linuxcnc/gcode/browser-custom.ngc",
"custom G-code path",
);
assertEqual(
gcodeFilenameFromProgramPath("linuxcnc/gcode/browser-custom.ngc"),
"browser-custom.ngc",
"custom G-code filename from OPFS path",
);
await assertRejects(
"browser G-code invalid program OPFS path",
() => Promise.resolve(gcodeFilenameFromProgramPath("linuxcnc/machines/browser-custom.ngc")),
/Invalid G-code program OPFS path/,
);
await saveGcodeProgram("browser-smoke.ngc", "G0 X0 Y0\nM2\n");
assertEqual(await loadGcodeProgram("browser-smoke.ngc"), "G0 X0 Y0\nM2\n", "gcode text");
await saveGcodeProgram("browser-custom.ngc", "G1 X1 F10\nM2\n");

View File

@@ -25,6 +25,7 @@ import {
validateSessionSnapshot,
} from "../../../runtime/opfs/snapshot-store.js";
import {
gcodeFilenameFromProgramPath,
loadGcodeProgram,
loadMachineTextFiles,
machineFilePaths,
@@ -201,6 +202,18 @@ assertGcodeProgramStoragePath(
"custom-fixture.ngc",
gcodeProgramPath("custom-fixture.ngc"),
);
assert.equal(
gcodeFilenameFromProgramPath("linuxcnc/gcode/fixture.ngc"),
"fixture.ngc",
);
assert.throws(
() => gcodeFilenameFromProgramPath("linuxcnc/machines/xyzab-tdr/fixture.ngc"),
/Invalid G-code program OPFS path/,
);
assert.throws(
() => gcodeFilenameFromProgramPath("linuxcnc/gcode/nested/fixture.ngc"),
/Invalid G-code program OPFS path/,
);
assertPreviewCacheStoragePath("fixture", previewCachePath("fixture"));
assertPreviewCacheStoragePath(
"fixture",