From 52e54f808965c28b9d73dd46a840f1bdd48db581 Mon Sep 17 00:00:00 2001 From: mes123456 Date: Thu, 2 Jul 2026 10:10:54 -0400 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=20xyzbc-trt=20OPFS=20?= =?UTF-8?q?=E4=B8=8E=20AXIS=20=E9=A6=96=E5=B1=8F=E7=AD=89=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gptlog-process/gpdlog.md | 27 +++ .../app/src/main.js | 2 +- .../app/src/runtime/five-axis-session.js | 15 +- .../runtime/linuxcnc-machine-file-staging.js | 15 +- .../app/src/runtime/tool-db-simulation.js | 15 +- .../app/src/state/store.js | 24 ++- .../app/src/styles/gmoccapy.css | 36 ++++ .../app/src/ui/gmoccapy-shell.js | 112 ++++++++++ .../browser/xyzbc_trt_browser_smoke.html | 88 +++++++- .../tools/collect-web-xyzbc-trt-evidence.mjs | 102 +++++++++ .../tools/compare-xyzbc-trt-evidence.mjs | 3 + .../working/03-推进台账.md | 46 ++++ .../working/04-任务矩阵.md | 10 +- .../working/05-验收证据.md | 53 +++++ .../evidence/compare-xyzbc-trt-evidence.json | 201 ++++++++++++++++- .../evidence/web-xyzbc-trt-evidence.json | 204 +++++++++++++++++- 16 files changed, 925 insertions(+), 28 deletions(-) diff --git a/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md b/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md index 51867a3..ba41ba3 100644 --- a/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md +++ b/web-rtcp-5axis-sim-plan/gptlog-process/gpdlog.md @@ -10852,3 +10852,30 @@ nginx: configuration file /etc/nginx/nginx.conf test is successful 17. 执行 `git status --short`,确认提交推送后仅剩未跟踪生成目录 `app/dist/` 和 `tools/__pycache__/`。 18. 执行 `git log -1 --oneline` 和 `git ls-remote origin refs/heads/master`,确认本地最新提交和远端 `master` 均为 `8fd9952`。 19. 本轮结论:T-034 tool-offset 闭环相关源码、测试、证据和工作文档已提交并推送到云仓库;构建产物和 Python 缓存未提交。 + +## 2026-07-02 执行日志:xyzbc-trt Web OPFS 与 AXIS 首屏等效 + +本轮接续 `/home/mes123456/cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/working` 开发,用户明确要求“Web 的文件使用 OPFS 系统”。执行过程如下: + +1. 读取并确认当前项目状态:`working/03-推进台账.md`、`working/04-任务矩阵.md`、`app/src/runtime/linuxcnc-machine-file-staging.js`、`app/src/runtime/five-axis-session.js`、`app/src/state/store.js`、`tests/browser/xyzbc_trt_browser_smoke.html` 等文件。 +2. 发现现有浏览器环境具备 OPFS 时会使用 `navigator.storage.getDirectory()`,但仍允许 memory fallback;Node evidence 与 Node smoke 显式使用 `createMemorySessionStorage()`,不能代表真实浏览器 OPFS。 +3. 修改 `app/src/runtime/linuxcnc-machine-file-staging.js`,为 machine file staging 增加 `requireOpfs` 选项:当要求 OPFS 而浏览器 OPFS 不可用时直接报错,不再静默回退到 memory。 +4. 修改 `app/src/runtime/five-axis-session.js`,为 session snapshot 保存/恢复增加 `requireOpfs` 选项。 +5. 修改 `app/src/runtime/tool-db-simulation.js`,为 tool table 保存增加 `requireOpfs` 选项。 +6. 修改 `app/src/state/store.js`,增加 `withDefaultWebOpfsRequirement()`:浏览器 UI 默认要求 OPFS;显式传入 storage 或 Node 环境保持原测试能力。 +7. 修改 `app/src/main.js`,默认启动的 machine file seed 调用 `store.stageMachineFiles({ requireOpfs: true })`。 +8. 扩展 `tests/browser/xyzbc_trt_browser_smoke.html`:断言 machine file seed 和 store staging 的 `storageMode=opfs`,并从 OPFS 读回 `xyzbc_switchkins.ngc`,确认包含 `M428/M429`;调用 `api.saveSession()` 并从 OPFS 读回 session JSON,确认 `machineProfile=xyzbc-trt`;调用 `api.saveToolDb()` 并从 OPFS 读回 tool table,确认包含 `T2` 和 `Z10`。 +9. 同步完成 T-026 AXIS 首屏等效:修改 `app/src/ui/gmoccapy-shell.js` 和 `app/src/styles/gmoccapy.css`,增加首屏状态带和 `data-axis-ui-*` 验收标记,覆盖程序、坐标、状态、MDI、switchkins、override、tool、preview、execution、axis-buttons。 +10. 修改 `tools/collect-web-xyzbc-trt-evidence.mjs`,新增 `axisMainUi` evidence 与 `coverage.axisMainUiEquivalent`。说明:该 Node evidence 仍为内存采集,真实浏览器 OPFS 由 browser smoke 验证。 +11. 修改 `tools/compare-xyzbc-trt-evidence.mjs`,新增 AXIS 首屏等效检查。 +12. 运行验证:`npm --prefix app run smoke:node` 通过,输出 `xyzbc_trt_web_app_smoke=ok`。 +13. 运行验证:`npm --prefix app run build` 通过,输出 `gmoccapy_static_build=ok`。 +14. 运行验证:`npm --prefix app run smoke:browser` 通过,输出 `xyzbc_trt_browser_smoke=ok`;此验证包含真实浏览器 OPFS 写入和读回。 +15. 运行验证:`npm --prefix app run evidence:web` 通过,生成 `working/evidence/web-xyzbc-trt-evidence.json`。 +16. 运行验证:`npm --prefix app run evidence:compare` 生成真实失败报告,输出 `compare_xyzbc_trt_status=fail fail_count=5`;当前为 20/25 通过,剩余 5 项仍来自 native preview/execution path 样本缺失。 +17. 更新 `working/03-推进台账.md`,记录本轮 OPFS 强制与 AXIS 首屏等效实现。 +18. 更新 `working/04-任务矩阵.md`:T-006/T-014/T-016 增加真实 OPFS 验收描述;T-017 更新为 20/25;T-026 标记完成。 +19. 更新 `working/05-验收证据.md`,新增“Web OPFS 与 AXIS 首屏等效验收”章节。 +20. 保留既有未跟踪输出 `app/dist/` 和 `tools/__pycache__/`,未清理或回退用户/构建产物。 + +本轮结论:目标 Web app 的浏览器文件链路已经默认强制使用 OPFS,browser smoke 已证明 machine files、session snapshot、tool table 均写入 OPFS 并可读回。Node evidence 仍使用内存存储,仅作为非浏览器数据采集,不作为真实 Web 文件系统证明。 diff --git a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/main.js b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/main.js index 23c6fea..64ef0f8 100644 --- a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/main.js +++ b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/main.js @@ -93,7 +93,7 @@ async function ensureMachineFilesForProfile(store) { if (state.machineFileStaging?.status === "staged" && state.machineFileStaging?.profileId === state.machineProfile) { return state.machineFileStaging; } - return await store.stageMachineFiles(); + return await store.stageMachineFiles({ requireOpfs: true }); } catch (error) { return { apiName: "web-rtcp-5axis-machine-file-staging-seed", diff --git a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/five-axis-session.js b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/five-axis-session.js index c134765..070f8b1 100644 --- a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/five-axis-session.js +++ b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/five-axis-session.js @@ -184,18 +184,22 @@ export function detectBrowserStorageCapability(globalScope = globalThis) { function resolveSessionStorage(options = {}) { if (options.storage) { + const mode = options.storageMode || storageModeFor(options.storage); + if (options.requireOpfs === true && mode !== "opfs") { + throw new Error(`OPFS storage required for session snapshots, got ${mode}`); + } return { storage: options.storage, - mode: options.storageMode || storageModeFor(options.storage), + mode, capability: { apiName: "web-rtcp-5axis-storage-capability", - opfsAvailable: options.storageMode === "opfs", - opfsUnavailable: options.storageMode !== "opfs", + opfsAvailable: mode === "opfs", + opfsUnavailable: mode !== "opfs", secureContext: true, forcedUnavailable: false, hasNavigatorStorage: false, hasGetDirectory: typeof options.storage.getDirectory === "function", - fallbackMode: options.storageMode === "opfs" ? null : options.storageMode || "custom", + fallbackMode: mode === "opfs" ? null : mode || "custom", reason: "explicit_storage", }, }; @@ -209,6 +213,9 @@ function resolveSessionStorage(options = {}) { capability, }; } + if (options.requireOpfs === true) { + throw new Error(`OPFS storage required for session snapshots: ${capability.reason}`); + } browserMemorySessionStorage ??= createMemorySessionStorage(); return { storage: browserMemorySessionStorage, diff --git a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js index 1217af3..0a375fb 100644 --- a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js +++ b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/linuxcnc-machine-file-staging.js @@ -241,15 +241,19 @@ export function detectMachineFileStorageCapability(globalScope = globalThis) { function resolveMachineFileStorage(options = {}) { if (options.storage) { + const mode = options.storageMode || storageModeFor(options.storage); + if (options.requireOpfs === true && mode !== "opfs") { + throw new Error(`OPFS storage required for machine files, got ${mode}`); + } return { storage: options.storage, - mode: options.storageMode || storageModeFor(options.storage), + mode, capability: { apiName: "web-rtcp-5axis-machine-file-storage-capability", - opfsAvailable: options.storageMode === "opfs", - opfsUnavailable: options.storageMode !== "opfs", + opfsAvailable: mode === "opfs", + opfsUnavailable: mode !== "opfs", secureContext: true, - fallbackMode: options.storageMode === "opfs" ? null : options.storageMode || "custom", + fallbackMode: mode === "opfs" ? null : mode || "custom", reason: "explicit_storage", }, }; @@ -262,6 +266,9 @@ function resolveMachineFileStorage(options = {}) { capability, }; } + if (options.requireOpfs === true) { + throw new Error(`OPFS storage required for machine files: ${capability.reason}`); + } browserMemoryMachineFileStorage ??= createMemoryStorage(); return { storage: browserMemoryMachineFileStorage, diff --git a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/tool-db-simulation.js b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/tool-db-simulation.js index 41991c3..b23bff4 100644 --- a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/tool-db-simulation.js +++ b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/runtime/tool-db-simulation.js @@ -998,14 +998,18 @@ function basename(path = "") { function resolveToolDbStorage(options = {}) { if (options.storage) { + const mode = options.storageMode || storageModeFor(options.storage); + if (options.requireOpfs === true && mode !== "opfs") { + throw new Error(`OPFS storage required for tool DB, got ${mode}`); + } return { storage: options.storage, - mode: options.storageMode || storageModeFor(options.storage), + mode, capability: { apiName: "web-rtcp-5axis-tool-db-storage-capability", - opfsAvailable: options.storageMode === "opfs", - opfsUnavailable: options.storageMode !== "opfs", - fallbackMode: options.storageMode === "opfs" ? null : options.storageMode || "custom", + opfsAvailable: mode === "opfs", + opfsUnavailable: mode !== "opfs", + fallbackMode: mode === "opfs" ? null : mode || "custom", reason: "explicit_storage", }, }; @@ -1018,6 +1022,9 @@ function resolveToolDbStorage(options = {}) { capability, }; } + if (options.requireOpfs === true) { + throw new Error(`OPFS storage required for tool DB: ${capability.reason}`); + } browserMemoryToolDbStorage ??= createMemoryToolDbStorage(); return { storage: browserMemoryToolDbStorage, diff --git a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/state/store.js b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/state/store.js index 12624ca..a2aacf6 100644 --- a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/state/store.js +++ b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/state/store.js @@ -99,6 +99,14 @@ function createTaskHalStatusLoopState({ }; } +function withDefaultWebOpfsRequirement(options = {}) { + if (options.storage || options.requireOpfs !== undefined) { + return options; + } + const isBrowser = typeof globalThis.window === "object" || typeof globalThis.document === "object"; + return isBrowser ? { ...options, requireOpfs: true } : options; +} + const initialState = { machineProfile: "xyzbc-trt", availableProfiles: fiveAxisProfiles.map(({ id, title, traj, kinematicsModuleId, kinematics }) => ({ @@ -2061,13 +2069,15 @@ export function createSimulationStore(seed = {}) { const saveSession = async (options = {}) => { dispatch({ type: "SESSION_SAVE_STARTED" }); try { + const storageOptions = withDefaultWebOpfsRequirement(options); const sessionId = options.sessionId || state.sessionPersistence.sessionId; const filename = options.filename || state.sessionPersistence.filename; const payload = createFiveAxisSessionPayload(state); const { snapshot, path, storageMode, storageCapability } = await saveFiveAxisSessionSnapshot(sessionId, payload, { filename, - storage: options.storage, - storageMode: options.storageMode, + storage: storageOptions.storage, + storageMode: storageOptions.storageMode, + requireOpfs: storageOptions.requireOpfs, metadata: options.metadata, }); dispatch({ @@ -2087,12 +2097,14 @@ export function createSimulationStore(seed = {}) { const restoreSession = async (options = {}) => { dispatch({ type: "SESSION_RESTORE_STARTED" }); try { + const storageOptions = withDefaultWebOpfsRequirement(options); const sessionId = options.sessionId || state.sessionPersistence.sessionId; const filename = options.filename || state.sessionPersistence.filename; const { snapshot, path, storageMode, storageCapability } = await loadFiveAxisSessionSnapshot(sessionId, { filename, - storage: options.storage, - storageMode: options.storageMode, + storage: storageOptions.storage, + storageMode: storageOptions.storageMode, + requireOpfs: storageOptions.requireOpfs, }); dispatch({ type: "SESSION_RESTORE_COMPLETE", @@ -2113,8 +2125,10 @@ export function createSimulationStore(seed = {}) { const stageMachineFiles = async (options = {}) => { dispatch({ type: "MACHINE_FILE_STAGING_STARTED" }); try { + const storageOptions = withDefaultWebOpfsRequirement(options); const { plan, save } = await stageProfileMachineFiles(state.profile, { ...options, + ...storageOptions, iniText: options.iniText || state.linuxCncIniConfig?.sourceText, }); dispatch({ type: "MACHINE_FILE_STAGING_COMPLETE", plan, save }); @@ -2153,7 +2167,7 @@ export function createSimulationStore(seed = {}) { if (!state.toolDbSimulation) { throw new Error("tool DB simulation is not ready"); } - const save = await saveToolDbSimulation(state.toolDbSimulation, options); + const save = await saveToolDbSimulation(state.toolDbSimulation, withDefaultWebOpfsRequirement(options)); setState({ toolDbSimulation: save.toolDb, toolDbReadiness: createToolDbReadiness(save.toolDb), diff --git a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/styles/gmoccapy.css b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/styles/gmoccapy.css index 81e4a2b..d548a28 100644 --- a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/styles/gmoccapy.css +++ b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/styles/gmoccapy.css @@ -754,6 +754,38 @@ button[data-icon-name] span { background: #eeeae3; } +.axis-first-screen-strip { + display: grid; + grid-template-columns: repeat(5, minmax(96px, 1fr)); + gap: 3px; + padding: 5px 6px; + border-bottom: 1px solid #aaa59b; + background: #151515; + color: #e9f8e9; + font: 11px/1.2 "Courier New", monospace; +} + +.axis-first-screen-strip span { + min-width: 0; + overflow: hidden; + padding: 3px 5px; + border: 1px solid #3e5f43; + background: #071108; + text-overflow: ellipsis; + white-space: nowrap; +} + +.axis-first-screen-strip b { + color: var(--green); + font-weight: 700; +} + +.axis-first-screen-strip [data-axis-ui-capability-ready="false"] { + border-color: #7a6840; + background: #1b1508; + color: #f0d58c; +} + .tabs { display: grid; grid-template-columns: 1fr 1fr 1fr; @@ -1074,6 +1106,10 @@ button[data-icon-name] span { .spindle h2 { font-size: 12px; } + + .axis-first-screen-strip { + grid-template-columns: repeat(2, minmax(90px, 1fr)); + } } @media (max-height: 720px) { diff --git a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/ui/gmoccapy-shell.js b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/ui/gmoccapy-shell.js index 284608e..9873c9d 100644 --- a/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/ui/gmoccapy-shell.js +++ b/web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/ui/gmoccapy-shell.js @@ -50,6 +50,7 @@ export function mountGmoccapyShell(root, store) { } function render(regions, state, dispatch) { + renderShellEquivalenceDataset(regions, state); renderTitlebar(regions.titlebar, state); renderPreview(regions.preview, state, dispatch); renderDro(regions.dro, state); @@ -61,6 +62,26 @@ function render(regions, state, dispatch) { renderBottomControls(regions["bottom-controls"], state, dispatch); } +function renderShellEquivalenceDataset(regions, state) { + const sectionMap = { + preview: "preview-toolpath-and-machine-model", + dro: "coordinates-dro", + gcode: "program-and-mdi", + "status-sidebar": "status-mode-and-switchkins", + "info-tabs": "execution-tool-and-runtime-state", + override: "feed-rapid-override", + "spindle-coolant": "spindle-coolant", + "bottom-controls": "run-jog-home-controls", + }; + for (const [name, section] of Object.entries(sectionMap)) { + const element = regions[name]; + if (!element) continue; + element.dataset.axisUiSection = section; + element.dataset.axisUiProfile = state.machineProfile; + element.dataset.axisUiCoordinates = state.profile?.traj?.coordinates || ""; + } +} + function renderTitlebar(element, state) { const currentLine = currentGcodeExecutionLine(state); @@ -599,7 +620,20 @@ function renderInfoTabs(element, state) { const parityMatrix = state.linuxCncParityMatrix; const commSummary = createGmoccapyCommunicationSummary(); const halSummary = createGmoccapyHalSummary(); + const firstScreen = createAxisFirstScreenSummary(state); element.innerHTML = ` +
+ ${firstScreen.items.map((item) => ` + + ${escapeHtml(item.label)}${escapeHtml(item.value)} + + `).join("")} +