推进 INI 面板 restore/load session workflow helper

This commit is contained in:
2026-06-14 21:30:19 +08:00
parent ddc9a4e0f3
commit 8e8a319d78
5 changed files with 364 additions and 22 deletions

View File

@@ -302,3 +302,82 @@ wasm-port/tests/host/verify_host_smokes.sh
更新抽成可调用 workflow helper 更新抽成可调用 workflow helper
- 给 UI/API 暴露一个更完整的 machine session state snapshot用于上层应用直接消费 - 给 UI/API 暴露一个更完整的 machine session state snapshot用于上层应用直接消费
- 只在这些 workflow helper 的验证需要时,继续收敛局部测试胶水。 - 只在这些 workflow helper 的验证需要时,继续收敛局部测试胶水。
一、2026-06-14 继续执行记录INI panel restore/load session workflow helper
本轮按 `text11.txt` 第一原则继续推进实质性 UI/session persistence 能力,把
`Restore Session Snapshot` 和 `Restore And Load Session` 两条用户流程抽成可复用 workflow
helper而不是继续做局部 path/test glue 收敛。
完成内容:
- 扩展 `wasm-port/runtime/ui/ini-panel/session-workflow.js`
- 新增导出:
- `restoreMachineSessionSnapshotWorkflow(input)`
- `sessionSnapshotRestoreLogLines(summary)`
- restore workflow 通过注入函数执行:
- `loadMachineSessionSnapshot(...)`
- `loadMachineTextFiles(...)`
- 可选 `loadSession()`
- restore-only 返回:
- restored snapshot
- editor INI text
- snapshot summary
- OPFS field state
- restore log lines
- restore-and-load 返回:
- loaded session
- load summary
- WASM field state
- snapshot + OPFS-to-WASM load log lines
- `wasm-port/runtime/ui/ini-panel/app.js` 的 `Restore Session Snapshot` 和
`Restore And Load Session` 按钮改为调用 workflow helper按钮处理器只负责写 editor、
field 和 log
- `wasm-port/tests/ui/node/verify_ini_panel_session_workflow.mjs` 覆盖 save、restore-only
和 restore-and-load 三条 workflow
- `wasm-port/docs/ui-session-summary-helpers.md` 更新 restore workflow API、返回结构、
log helper 和边界说明;
- 未改变 OPFS bridge、snapshot-store、machine-file-store、LinuxCNC interpreter、tool、
parameter、G-code 文本或 LinuxCNC-owned runtime semantics。
验证已通过:
```bash
git diff --check
wasm-port/tests/ui/node/verify_ini_panel_session_workflow.sh
wasm-port/tests/ui/node/verify_ui_node_smokes.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
wasm-port/tests/host/verify_host_smokes.sh
```
关键输出:
```text
ini_panel_session_workflow_node_smoke=ok
ini_panel_run_summary_node_smoke=ok
ini_panel_session_summary_node_smoke=ok
ini_panel_machine_file_summary_node_smoke=ok
ini_panel_state_summary_node_smoke=ok
ui_node_smokes=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
host_wasm_opfs_browser_smokes=ok
```
下一步建议:
继续按“实质性能力优先”推进。下一批优先把 `run-gcode` 的用户流程抽成可调用 workflow
helper覆盖 snapshot/default program selection、G-code OPFS load、WASM write、LinuxCNC
interpreter run、run summary、field/log state 和 error state。该 helper 仍只能编排
host/runtime boundary不解析或实现 G-code、planner、tool、parameter 或 canonical-event
语义。

View File

@@ -8,9 +8,9 @@ for summarizing machine session snapshot state.
for machine-session load state. These helpers are pure so browser UI panels, for machine-session load state. These helpers are pure so browser UI panels,
SDK-facing wrappers, and Node smoke tests can share the same data shape without SDK-facing wrappers, and Node smoke tests can share the same data shape without
parsing log text. parsing log text.
`runtime/ui/ini-panel/session-workflow.js` contains the reusable UI workflow for `runtime/ui/ini-panel/session-workflow.js` contains the reusable UI workflows
saving the machine text files, default G-code, and machine session snapshot as for saving, restoring, and restore-loading a machine session snapshot as
one host-boundary operation. host-boundary operations.
The helpers do not implement CNC behavior. G-code execution, canonical events, The helpers do not implement CNC behavior. G-code execution, canonical events,
tool handling, parameters, kinematics, and planner behavior remain owned by the tool handling, parameters, kinematics, and planner behavior remain owned by the
@@ -72,6 +72,40 @@ The persistence functions are injected by the caller:
} }
``` ```
## `restoreMachineSessionSnapshotWorkflow(input)`
The workflow loads the machine session snapshot, loads the persisted machine
text files so the editor can be restored, and optionally loads the machine
session into the interpreter WASM filesystem.
When `loadSession` is omitted, the returned `fields` contain the OPFS snapshot
paths. When `loadSession` is supplied, the returned `fields` contain the loaded
WASM paths for INI/parameter/tool-table files and the snapshot G-code OPFS
path.
The returned object has this shape:
```js
{
snapshot: { ... },
editorIniText: "[EMC]\n...",
summary: { snapshotLabel: "...", workflow: "save-session-snapshot" },
fields: {
sessionIni: "/work/session-machine.ini",
sessionParameters: "/work/session-linuxcnc.var",
sessionToolTable: "/work/session-tool.tbl",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
sessionSnapshot: "ui-machine-session/ui-machine-session.json"
},
loadSummary: { iniWasmPath: "/work/session-machine.ini" },
loadedSession: { ... },
logLines: [
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot"
]
}
```
The returned object has this shape: The returned object has this shape:
```js ```js
@@ -102,6 +136,8 @@ The returned object has this shape:
OPFS-to-WASM mapping and captured result lines. OPFS-to-WASM mapping and captured result lines.
- `sessionSnapshotSaveLogLines(summary)` renders the save-workflow snapshot - `sessionSnapshotSaveLogLines(summary)` renders the save-workflow snapshot
paths used by the INI panel. paths used by the INI panel.
- `sessionSnapshotRestoreLogLines(summary)` renders the restore-workflow
snapshot paths used by the INI panel.
## Boundary Rules ## Boundary Rules

View File

@@ -42,6 +42,7 @@ import {
sessionSnapshotSummaryLogLines, sessionSnapshotSummaryLogLines,
} from "./session-summary.js"; } from "./session-summary.js";
import { import {
restoreMachineSessionSnapshotWorkflow,
saveMachineSessionSnapshotWorkflow, saveMachineSessionSnapshotWorkflow,
} from "./session-workflow.js"; } from "./session-workflow.js";
@@ -600,16 +601,24 @@ document.getElementById("save-session-snapshot").addEventListener("click", async
document.getElementById("restore-session-snapshot").addEventListener("click", async () => { document.getElementById("restore-session-snapshot").addEventListener("click", async () => {
try { try {
const snapshot = await restoreSessionSnapshotToEditor(); const workflow = await restoreMachineSessionSnapshotWorkflow({
const summary = createSessionSnapshotSummary(snapshot, sessionSnapshotLabel(snapshot.sessionId)); machineId: MACHINE_ID,
snapshotId: UI_SESSION.snapshotId,
snapshotFilename: UI_SESSION.snapshotFilename,
snapshotLabel: sessionSnapshotLabel(UI_SESSION),
loadMachineSessionSnapshot,
loadMachineTextFiles,
});
editor.value = workflow.editorIniText;
setField("sessionIni", workflow.fields.sessionIni);
setField("sessionParameters", workflow.fields.sessionParameters);
setField("sessionToolTable", workflow.fields.sessionToolTable);
setField("sessionGcode", workflow.fields.sessionGcode);
setField("sessionSnapshot", workflow.fields.sessionSnapshot);
setLog( setLog(
[ [
"Restored machine session snapshot from OPFS.", "Restored machine session snapshot from OPFS.",
...sessionSnapshotSummaryLogLines(summary), ...workflow.logLines,
`INI: ${summary.iniOpfsPath}`,
`Parameter file: ${summary.parameterOpfsPath}`,
`Tool table file: ${summary.toolTableOpfsPath}`,
`G-code: ${summary.gcodeOpfsPath ?? "-"}`,
].join("\n"), ].join("\n"),
); );
} catch (error) { } catch (error) {
@@ -634,17 +643,25 @@ document.getElementById("load-session").addEventListener("click", async () => {
document.getElementById("restore-load-session").addEventListener("click", async () => { document.getElementById("restore-load-session").addEventListener("click", async () => {
try { try {
const snapshot = await restoreSessionSnapshotToEditor(); const workflow = await restoreMachineSessionSnapshotWorkflow({
await loadMachineSessionIntoWasm(); machineId: MACHINE_ID,
setField("sessionGcode", snapshot.payload.files.gcode); snapshotId: UI_SESSION.snapshotId,
const summary = createSessionSnapshotSummary(snapshot, sessionSnapshotLabel(snapshot.sessionId)); snapshotFilename: UI_SESSION.snapshotFilename,
const loadSummary = createMachineSessionLoadSummary(loadedSession); snapshotLabel: sessionSnapshotLabel(UI_SESSION),
loadMachineSessionSnapshot,
loadMachineTextFiles,
loadSession: () => loadMachineSessionIntoWasm(),
});
editor.value = workflow.editorIniText;
setField("sessionIni", workflow.fields.sessionIni);
setField("sessionParameters", workflow.fields.sessionParameters);
setField("sessionToolTable", workflow.fields.sessionToolTable);
setField("sessionGcode", workflow.fields.sessionGcode);
setField("sessionSnapshot", workflow.fields.sessionSnapshot);
setLog( setLog(
[ [
"Restored and loaded machine session snapshot.", "Restored and loaded machine session snapshot.",
...sessionSnapshotSummaryLogLines(summary), ...workflow.logLines,
...machineSessionLoadLogLines(loadSummary),
`G-code: ${summary.gcodeOpfsPath ?? "-"}`,
].join("\n"), ].join("\n"),
); );
} catch (error) { } catch (error) {

View File

@@ -1,3 +1,7 @@
import {
createMachineSessionLoadSummary,
machineSessionLoadLogLines,
} from "./session-load-summary.js";
import { import {
createSessionSnapshotSummary, createSessionSnapshotSummary,
sessionSnapshotSummaryLogLines, sessionSnapshotSummaryLogLines,
@@ -23,6 +27,16 @@ export function sessionSnapshotSaveLogLines(summary) {
]; ];
} }
export function sessionSnapshotRestoreLogLines(summary) {
return [
...sessionSnapshotSummaryLogLines(summary),
`INI: ${summary.iniOpfsPath}`,
`Parameter file: ${summary.parameterOpfsPath}`,
`Tool table file: ${summary.toolTableOpfsPath}`,
`G-code: ${summary.gcodeOpfsPath ?? "-"}`,
];
}
export async function saveMachineSessionSnapshotWorkflow({ export async function saveMachineSessionSnapshotWorkflow({
machineId, machineId,
snapshotId, snapshotId,
@@ -63,3 +77,54 @@ export async function saveMachineSessionSnapshotWorkflow({
logLines: sessionSnapshotSaveLogLines(summary), logLines: sessionSnapshotSaveLogLines(summary),
}; };
} }
export async function restoreMachineSessionSnapshotWorkflow({
machineId,
snapshotId,
snapshotFilename,
snapshotLabel,
loadMachineSessionSnapshot,
loadMachineTextFiles,
loadSession = null,
}) {
const snapshot = await loadMachineSessionSnapshot(
snapshotId,
machineId,
{ filename: snapshotFilename },
);
const files = await loadMachineTextFiles(machineId);
const summary = createSessionSnapshotSummary(snapshot, snapshotLabel ?? snapshotLabelFromSession(snapshot));
const fields = createSessionSnapshotFieldState(summary);
const result = {
snapshot,
editorIniText: files.ini,
summary,
fields,
loadSummary: null,
loadedSession: null,
logLines: sessionSnapshotRestoreLogLines(summary),
};
if (loadSession) {
const loadedSession = await loadSession();
const loadSummary = createMachineSessionLoadSummary(loadedSession);
result.loadedSession = loadedSession;
result.loadSummary = loadSummary;
result.fields = {
...fields,
sessionIni: loadSummary.iniWasmPath,
sessionParameters: loadSummary.parameterWasmPath,
sessionToolTable: loadSummary.toolTableWasmPath,
sessionGcode: summary.gcodeOpfsPath,
};
result.logLines = [
...sessionSnapshotSummaryLogLines(summary),
...machineSessionLoadLogLines(loadSummary),
`G-code: ${summary.gcodeOpfsPath ?? "-"}`,
];
}
return result;
}
function snapshotLabelFromSession(snapshot) {
return snapshot?.metadata?.snapshotLabel ?? snapshot?.sessionId ?? null;
}

View File

@@ -1,11 +1,12 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { import {
restoreMachineSessionSnapshotWorkflow,
saveMachineSessionSnapshotWorkflow, saveMachineSessionSnapshotWorkflow,
} from "../../../runtime/ui/ini-panel/session-workflow.js"; } from "../../../runtime/ui/ini-panel/session-workflow.js";
const calls = []; const calls = [];
const workflow = await saveMachineSessionSnapshotWorkflow({ const saveWorkflow = await saveMachineSessionSnapshotWorkflow({
machineId: "xyzab-tdr", machineId: "xyzab-tdr",
snapshotId: "ui-machine-session", snapshotId: "ui-machine-session",
snapshotFilename: "ui-machine-session.json", snapshotFilename: "ui-machine-session.json",
@@ -76,7 +77,7 @@ assert.deepEqual(calls, [
], ],
]); ]);
assert.deepEqual(workflow.summary, { assert.deepEqual(saveWorkflow.summary, {
snapshotLabel: "ui-machine-session/ui-machine-session.json", snapshotLabel: "ui-machine-session/ui-machine-session.json",
workflow: "save-session-snapshot", workflow: "save-session-snapshot",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc", defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
@@ -86,7 +87,7 @@ assert.deepEqual(workflow.summary, {
gcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc", gcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
}); });
assert.deepEqual(workflow.fields, { assert.deepEqual(saveWorkflow.fields, {
sessionIni: "linuxcnc/machines/xyzab-tdr/machine.ini", sessionIni: "linuxcnc/machines/xyzab-tdr/machine.ini",
sessionParameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var", sessionParameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
sessionToolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl", sessionToolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
@@ -94,7 +95,7 @@ assert.deepEqual(workflow.fields, {
sessionSnapshot: "ui-machine-session/ui-machine-session.json", sessionSnapshot: "ui-machine-session/ui-machine-session.json",
}); });
assert.deepEqual(workflow.logLines, [ assert.deepEqual(saveWorkflow.logLines, [
"Snapshot: ui-machine-session/ui-machine-session.json", "Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot", "Workflow: save-session-snapshot",
"Default G-code: linuxcnc/gcode/ui-session.ngc", "Default G-code: linuxcnc/gcode/ui-session.ngc",
@@ -104,4 +105,148 @@ assert.deepEqual(workflow.logLines, [
"G-code: linuxcnc/gcode/ui-session.ngc", "G-code: linuxcnc/gcode/ui-session.ngc",
]); ]);
const restoreCalls = [];
const restoreWorkflow = await restoreMachineSessionSnapshotWorkflow({
machineId: "xyzab-tdr",
snapshotId: "ui-machine-session",
snapshotFilename: "ui-machine-session.json",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
loadMachineSessionSnapshot: async (snapshotId, machineId, options) => {
restoreCalls.push(["loadMachineSessionSnapshot", snapshotId, machineId, options]);
return {
sessionId: snapshotId,
metadata: {
workflow: "save-session-snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
},
payload: {
files: {
ini: "linuxcnc/machines/xyzab-tdr/machine.ini",
parameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
gcode: "linuxcnc/gcode/ui-session.ngc",
},
},
};
},
loadMachineTextFiles: async (machineId) => {
restoreCalls.push(["loadMachineTextFiles", machineId]);
return {
ini: "[EMC]\nMACHINE = xyzab-tdr\n",
};
},
});
assert.deepEqual(restoreCalls, [
[
"loadMachineSessionSnapshot",
"ui-machine-session",
"xyzab-tdr",
{ filename: "ui-machine-session.json" },
],
["loadMachineTextFiles", "xyzab-tdr"],
]);
assert.deepEqual(restoreWorkflow.fields, {
sessionIni: "linuxcnc/machines/xyzab-tdr/machine.ini",
sessionParameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
sessionToolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
});
assert.deepEqual(restoreWorkflow.logLines, [
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot",
"Default G-code: linuxcnc/gcode/ui-session.ngc",
"INI: linuxcnc/machines/xyzab-tdr/machine.ini",
"Parameter file: linuxcnc/machines/xyzab-tdr/linuxcnc.var",
"Tool table file: linuxcnc/machines/xyzab-tdr/tool.tbl",
"G-code: linuxcnc/gcode/ui-session.ngc",
]);
const restoreLoadCalls = [];
const restoreLoadWorkflow = await restoreMachineSessionSnapshotWorkflow({
machineId: "xyzab-tdr",
snapshotId: "ui-machine-session",
snapshotFilename: "ui-machine-session.json",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
loadMachineSessionSnapshot: async (snapshotId, machineId, options) => {
restoreLoadCalls.push(["loadMachineSessionSnapshot", snapshotId, machineId, options]);
return {
sessionId: snapshotId,
metadata: {
workflow: "save-session-snapshot",
snapshotLabel: "ui-machine-session/ui-machine-session.json",
defaultGcodeOpfsPath: "linuxcnc/gcode/ui-session.ngc",
},
payload: {
files: {
ini: "linuxcnc/machines/xyzab-tdr/machine.ini",
parameters: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
toolTable: "linuxcnc/machines/xyzab-tdr/tool.tbl",
gcode: "linuxcnc/gcode/ui-session.ngc",
},
},
};
},
loadMachineTextFiles: async (machineId) => {
restoreLoadCalls.push(["loadMachineTextFiles", machineId]);
return {
ini: "[EMC]\nMACHINE = xyzab-tdr\n",
};
},
loadSession: async () => {
restoreLoadCalls.push(["loadSession"]);
return {
ini: {
opfsPath: "linuxcnc/machines/xyzab-tdr/machine.ini",
wasmPath: "/work/session-machine.ini",
},
parameters: {
opfsPath: "linuxcnc/machines/xyzab-tdr/linuxcnc.var",
wasmPath: "/work/session-linuxcnc.var",
result: "restore_parameters=0",
},
toolTable: {
opfsPath: "linuxcnc/machines/xyzab-tdr/tool.tbl",
wasmPath: "/work/session-tool.tbl",
result: "tooldata_load=0",
},
};
},
});
assert.deepEqual(restoreLoadCalls, [
[
"loadMachineSessionSnapshot",
"ui-machine-session",
"xyzab-tdr",
{ filename: "ui-machine-session.json" },
],
["loadMachineTextFiles", "xyzab-tdr"],
["loadSession"],
]);
assert.deepEqual(restoreLoadWorkflow.fields, {
sessionIni: "/work/session-machine.ini",
sessionParameters: "/work/session-linuxcnc.var",
sessionToolTable: "/work/session-tool.tbl",
sessionGcode: "linuxcnc/gcode/ui-session.ngc",
sessionSnapshot: "ui-machine-session/ui-machine-session.json",
});
assert.deepEqual(restoreLoadWorkflow.logLines, [
"Snapshot: ui-machine-session/ui-machine-session.json",
"Workflow: save-session-snapshot",
"Default G-code: linuxcnc/gcode/ui-session.ngc",
"INI: linuxcnc/machines/xyzab-tdr/machine.ini -> /work/session-machine.ini",
"Parameter file: linuxcnc/machines/xyzab-tdr/linuxcnc.var -> /work/session-linuxcnc.var",
"Parameters: restore_parameters=0",
"Tool table file: linuxcnc/machines/xyzab-tdr/tool.tbl -> /work/session-tool.tbl",
"Tool table: tooldata_load=0",
"G-code: linuxcnc/gcode/ui-session.ngc",
]);
console.log("ini_panel_session_workflow_node_smoke=ok"); console.log("ini_panel_session_workflow_node_smoke=ok");