瀏覽代碼

test(guide): add closure review, live checks and export evidence

main
leiyun 1 周之前
父節點
當前提交
3ddf591f8a
共有 92 個檔案被更改,包括 10000 行新增0 行删除
  1. 二進制
     
  2. 二進制
     
  3. 二進制
     
  4. 二進制
     
  5. 二進制
     
  6. 二進制
     
  7. 二進制
     
  8. 二進制
     
  9. 二進制
     
  10. 二進制
     
  11. 二進制
     
  12. 二進制
     
  13. 二進制
     
  14. 二進制
     
  15. 二進制
     
  16. 二進制
     
  17. 二進制
     
  18. 二進制
     
  19. 二進制
     
  20. 二進制
     
  21. 二進制
     
  22. 二進制
     
  23. 二進制
     
  24. 二進制
     
  25. 二進制
     
  26. 二進制
     
  27. 二進制
     
  28. 二進制
     
  29. 二進制
     
  30. 二進制
     
  31. 二進制
     
  32. 二進制
     
  33. +56
    -0
      reports/guide-closure-20260910/api.json
  34. +34
    -0
      reports/guide-closure-20260910/artifact-validation.json
  35. 二進制
     
  36. +16
    -0
      reports/guide-closure-20260910/content.json
  37. +21
    -0
      reports/guide-closure-20260910/create.json
  38. +41
    -0
      reports/guide-closure-20260910/edit.json
  39. 二進制
     
  40. 二進制
     
  41. 二進制
     
  42. 二進制
     
  43. +71
    -0
      reports/guide-closure-20260910/export-plan.md
  44. +7677
    -0
      reports/guide-closure-20260910/export-project.json
  45. +87
    -0
      reports/guide-closure-20260910/export-requirement.md
  46. +36
    -0
      reports/guide-closure-20260910/export.json
  47. +21
    -0
      reports/guide-closure-20260910/final-document.json
  48. 二進制
     
  49. 二進制
     
  50. 二進制
     
  51. 二進制
     
  52. +50
    -0
      reports/guide-closure-20260910/index.html
  53. +5
    -0
      reports/guide-closure-20260910/inspect.json
  54. 二進制
     
  55. +41
    -0
      reports/guide-closure-20260910/layout.json
  56. 二進制
     
  57. 二進制
     
  58. +56
    -0
      reports/guide-closure-20260910/media.json
  59. 二進制
     
  60. +11
    -0
      reports/guide-closure-20260910/missing.json
  61. 二進制
     
  62. +41
    -0
      reports/guide-closure-20260910/offline.json
  63. 二進制
     
  64. 二進制
     
  65. 二進制
     
  66. 二進制
     
  67. 二進制
     
  68. 二進制
     
  69. 二進制
     
  70. 二進制
     
  71. 二進制
     
  72. 二進制
     
  73. 二進制
     
  74. 二進制
     
  75. 二進制
     
  76. 二進制
     
  77. +26
    -0
      reports/guide-closure-20260910/reader.json
  78. 二進制
     
  79. 二進制
     
  80. +31
    -0
      reports/guide-closure-20260910/resources.json
  81. +46
    -0
      reports/guide-closure-20260910/revision.json
  82. +905
    -0
      reports/guide-closure-20260910/summary.json
  83. 二進制
     
  84. 二進制
     
  85. +81
    -0
      reports/guide-closure-20260910/templates.json
  86. 二進制
     
  87. +5
    -0
      reports/guide-closure-20260910/viewport-probe.json
  88. 二進制
     
  89. 二進制
     
  90. +39
    -0
      tools/guide-artifact-check.py
  91. +510
    -0
      tools/guide-closure-live.mjs
  92. +93
    -0
      tools/guide-closure-report.mjs

二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


+ 56
- 0
reports/guide-closure-20260910/api.json 查看文件

@@ -0,0 +1,56 @@
{
"checks": [
{
"name": "对外接口展示九个操作",
"status": "PASS",
"detail": ""
},
{
"name": "本地接口调度 list-projects 返回成功",
"status": "PASS",
"detail": "200\nGET /api/ofd/projects\n10 ms\n{\n \"success\": true,\n \"data\": {\n \"total\": 3,"
},
{
"name": "本地接口调度 get-project 返回成功",
"status": "PASS",
"detail": "200\nGET /api/ofd/projects/OFD-mtub76ed-01\n8 ms\n{\n \"success\": true,\n \"data\": {\n"
},
{
"name": "本地接口调度 update-step 返回成功",
"status": "PASS",
"detail": "200\nPUT /api/ofd/projects/OFD-mtub76ed-01/steps/step-01\n26 ms\n{\n \"success\": tru"
},
{
"name": "本地接口调度 register-resource 返回成功",
"status": "PASS",
"detail": "201\nPOST /api/ofd/projects/OFD-mtub76ed-01/resources\n35 ms\n{\n \"success\": true,\n"
},
{
"name": "本地接口调度 collect-resources 返回成功",
"status": "PASS",
"detail": "201\nPOST /api/ofd/projects/OFD-mtub76ed-01/collect\n44 ms\n{\n \"success\": true,\n "
},
{
"name": "本地接口调度 inspect 返回成功",
"status": "PASS",
"detail": "200\nPOST /api/ofd/projects/OFD-mtub76ed-01/inspect\n9 ms\n{\n \"success\": true,\n \""
},
{
"name": "本地接口调度 delivery-manifest 返回成功",
"status": "PASS",
"detail": "200\nPOST /api/ofd/projects/OFD-mtub76ed-01/delivery-manifest\n6 ms\n{\n \"success\":"
},
{
"name": "本地接口调度 submit-feedback 返回成功",
"status": "PASS",
"detail": "201\nPOST /api/ofd/projects/OFD-mtub76ed-01/feedback\n25 ms\n{\n \"success\": true,\n "
},
{
"name": "接口面板未发送真实 HTTP 业务请求",
"status": "PASS",
"detail": "这里通过本地函数调度,需要接入正式 API 才可外部调用"
}
],
"errors": [],
"requests": []
}

+ 34
- 0
reports/guide-closure-20260910/artifact-validation.json 查看文件

@@ -0,0 +1,34 @@
{
"checks": [
{
"name": "PDF 四页可解析且每页含文本",
"status": "PASS",
"detail": "4 pages"
},
{
"name": "OFD ZIP 完整、入口和 XML 可解析",
"status": "PASS",
"detail": "9 XML files; 未使用第三方 OFD 阅读器认证"
},
{
"name": "离线 ZIP 完整",
"status": "PASS",
"detail": "16 files"
},
{
"name": "离线脚本没有 Vite 开发缓存引用",
"status": "PASS",
"detail": ""
},
{
"name": "离线狐狸和卡车 GLB 结构及动画保留",
"status": "PASS",
"detail": "导出按现有逻辑轻量化,卡车从 369980 缩至 318276 字节;非原文件逐字节复制。实际渲染和动画另由浏览器验证。"
},
{
"name": "离线包含本地 Three.js 和解码器",
"status": "PASS",
"detail": ""
}
]
}

二進制
查看文件


+ 16
- 0
reports/guide-closure-20260910/content.json 查看文件

@@ -0,0 +1,16 @@
{
"checks": [
{
"name": "公式与说明保存",
"status": "PASS",
"detail": ""
},
{
"name": "发布保存本地正式版本",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
}

+ 21
- 0
reports/guide-closure-20260910/create.json 查看文件

@@ -0,0 +1,21 @@
{
"checks": [
{
"name": "通过界面从空白模板创建指导书",
"status": "PASS",
"detail": ""
},
{
"name": "创建调用正式内容接口",
"status": "FAIL",
"detail": "当前页面使用浏览器本地工程库"
},
{
"name": "新建工程出现在正式后端列表",
"status": "FAIL",
"detail": ""
}
],
"errors": [],
"requests": []
}

+ 41
- 0
reports/guide-closure-20260910/edit.json 查看文件

@@ -0,0 +1,41 @@
{
"checks": [
{
"name": "步骤标题、说明、工具、安全、验收编辑保存",
"status": "PASS",
"detail": ""
},
{
"name": "刷新后恢复已保存的本地内容",
"status": "PASS",
"detail": ""
},
{
"name": "新增步骤后保存",
"status": "PASS",
"detail": ""
},
{
"name": "正文续页和复制页面保存",
"status": "PASS",
"detail": ""
},
{
"name": "撤销复制恢复页数",
"status": "PASS",
"detail": ""
},
{
"name": "重做复制恢复页数",
"status": "PASS",
"detail": ""
},
{
"name": "导入真实 Fox.glb 文件并登记本地资源",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
}

二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


+ 71
- 0
reports/guide-closure-20260910/export-plan.md 查看文件

@@ -0,0 +1,71 @@
# 闭环实测-狐狸巡检作业指导-20260910 构建计划书

> 文档编号:REVIEW-GUIDE-20260910  版本:V0.1.0
> 适用装备:狐狸模型测试装备  编制单位:内部维护
> 执行规范:GB/T 33190-2016  生成日期:2026-09-10
> 密级:内部资料 · 注意保存

本文件由数字车间作业指导书模块按当前工程状态自动生成,内容随工程更新而变化。

## 一、构建依据

- 内容模板:空白指导书
- 资料来源:支腿液压缸中修资料包.hg
- 章节步骤规模:1 章 / 2 步

## 二、构建阶段与交付物

| 阶段 | 主要工作 | 交付物 | 完成判定 |
| --- | --- | --- | --- |
| 内容分析 | 资料结构识别、章节层级划分、媒体资源索引 | 需求规格说明书、构建计划书 | 两份文档可预览与下载 |
| 内容编制 | 章节步骤编写、安全要求与技术参数录入 | 步骤内容初稿 | 输出前检查的内容类项目全部通过 |
| 三维关联 | 部件选取、空间锚定、热点与动画区间绑定 | 三维关联配置 | 每个步骤具备关联对象与锚点 |
| 输出前检查 | 逐项核对元数据、结构、资源与版式 | 检查报告 | 建议项清晰可定位,不影响导出 |
| 生成与发布 | 生成 .hg 包、校验目录与 CRC、版本归档 | 正式包与版本记录 | 包体通过结构校验 |
| 阅读交付 | 内网交互阅读与单机离线阅读 | 阅读端入口 | 预览打开 < 3 秒 |

## 三、章节构建计划

| 章节 | 步骤数 | 预计页数 | 已关联三维对象 | 已配置插图 |
| --- | --- | --- | --- | --- |
| 01 新建章节 | 2 | 4 | 0/2 | 0/2 |

## 四、资源准备计划

| 类型 | 名称 | 当前状态 | 处理方式 |
| --- | --- | --- | --- |
| 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 |
| 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 |
| 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 |
| 三维模型 | 08-CesiumMilkTruck.glb | 已持久化 | 直接引用 |
| 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 |
| 三维模型 | 08-CesiumMilkTruck.glb | 已持久化 | 直接引用 |
| 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 |
| 三维模型 | 08-CesiumMilkTruck.glb | 已持久化 | 直接引用 |
| 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 |
| 三维模型 | 08-CesiumMilkTruck.glb | 已持久化 | 直接引用 |
| 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 |
| 三维模型 | 08-CesiumMilkTruck.glb | 已持久化 | 直接引用 |
| 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 |
| 三维模型 | 08-CesiumMilkTruck.glb | 已持久化 | 直接引用 |
| 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 |
| 三维模型 | 08-CesiumMilkTruck.glb | 已持久化 | 直接引用 |
| 三维模型 | 09-Fox.glb | 已持久化 | 直接引用 |
| 三维模型 | 08-CesiumMilkTruck.glb | 已持久化 | 直接引用 |

## 五、资源采集状态

尚未运行内容采集。规划阶段可从平台资源库为每个步骤匹配候选资源,确认后写入步骤。

## 六、当前内容关联度

- 已配插图:0/2 个步骤
- 已绑模型资产:2/2 个步骤
- 含版式标注:0/2 个步骤

## 七、风险与依赖

- 未内嵌字体:在无宋体、无黑体的目标环境(如银河麒麟)可能显示替换字形,需在信创交付前补字体子集
- 三维交互能力由动态版式承载,OFD 包仅保证静态版式
- OFD 加密、CA/国密数字签名、可信时间戳与长期归档是原始交付要求;当前内容完整性校验不能替代,需接入甲方证书及归档环境后联调验收
- 内容素材(文字、规程、图表)由甲方或教研方提供

+ 7677
- 0
reports/guide-closure-20260910/export-project.json
文件差異過大導致無法顯示
查看文件


+ 87
- 0
reports/guide-closure-20260910/export-requirement.md 查看文件

@@ -0,0 +1,87 @@
# 闭环实测-狐狸巡检作业指导-20260910 需求规格说明书

> 文档编号:REVIEW-GUIDE-20260910  版本:V0.1.0
> 适用装备:狐狸模型测试装备  编制单位:内部维护
> 执行规范:GB/T 33190-2016  生成日期:2026-09-10
> 密级:内部资料 · 注意保存

本文件由数字车间作业指导书模块按当前工程状态自动生成,内容随工程更新而变化。

## 一、编制目的与范围

本作业指导书面向 狐狸模型测试装备 的维修作业,覆盖「拆、装、检、排」四位一体作业内容,
共 1 个作业章节、2 个作业步骤。

输出格式遵循国家标准 GB/T 33190-2016,交付扩展名为 .hg。

## 二、内容需求清单

| 章节 | 步骤 | 类型 | 参考时长 | 状态 |
| --- | --- | --- | --- | --- |
| 01 新建章节 | 狐狸巡检目标识别(修订) | 其他 | 01:00 | 待完善 |
| 01 新建章节 | 牛奶卡车状态检查 | 作业步骤 | 01:00 | 待完善 |

## 三、安全要求

| 编号 | 级别 | 要求 | 说明 |
| --- | --- | --- | --- |
| SR-01 | 禁止 | 执行停机挂牌与液压卸压 | 未完成能量隔离和双人复核不得开始拆装。 |
| SR-02 | 警告 | 禁止进入悬吊物下方 | 吊装、承托与移运过程中保持安全站位。 |
| SR-03 | 注意 | 液压介质污染控制 | 断开的油管与接头必须即时封堵并保持清洁。 |
| SR-04 | 注意 | 禁止用手指探查对孔 | 装配对孔必须使用定位棒或规定工装。 |

## 四、技术参数要求

| 编号 | 参数 | 取值 | 单位 | 依据 |
| --- | --- | --- | --- | --- |
| TP-01 | 连接销紧固扭矩 | 180 ± 10 | N·m | 维修工艺卡 ZC-WX-17 |
| TP-02 | 油管接头紧固扭矩 | 65 ± 5 | N·m | 液压接口规范 ZC-HY-04 |
| TP-03 | 系统卸压判定 | ≤ 0.2 | MPa | 安全作业规程 ZC-AQ-02 |
| TP-04 | 试运行保压时间 | 5 | min | 维修验收规范 ZC-YS-09 |

## 五、数字资源需求

| 类型 | 名称 | 体积 | 状态 | 引用次数 |
| --- | --- | --- | --- | --- |
| 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 |
| 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 |
| 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 |
| 三维模型 | 08-CesiumMilkTruck.glb | 0.4 MB | 已持久化 | 0 |
| 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 |
| 三维模型 | 08-CesiumMilkTruck.glb | 0.4 MB | 已持久化 | 0 |
| 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 |
| 三维模型 | 08-CesiumMilkTruck.glb | 0.4 MB | 已持久化 | 0 |
| 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 |
| 三维模型 | 08-CesiumMilkTruck.glb | 0.4 MB | 已持久化 | 0 |
| 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 |
| 三维模型 | 08-CesiumMilkTruck.glb | 0.4 MB | 已持久化 | 0 |
| 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 |
| 三维模型 | 08-CesiumMilkTruck.glb | 0.4 MB | 已持久化 | 0 |
| 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 |
| 三维模型 | 08-CesiumMilkTruck.glb | 0.4 MB | 已持久化 | 0 |
| 三维模型 | 09-Fox.glb | 0.2 MB | 已持久化 | 0 |
| 三维模型 | 08-CesiumMilkTruck.glb | 0.4 MB | 已持久化 | 0 |

## 六、输出与验收要求

- 输出格式:OFD 静态版式,遵循 GB/T 33190-2016
- 版式要素:密级标识、页眉红线、封面文档信息表、页脚页码、可选可视印章位
- 在线预览打开时间:< 3 秒
- 支持离线阅读,排版不受影响
- 三维交互、动画与热点由动态版式承载,不作为 OFD 原生交互能力声明

## 七、当前改进建议

| 检查项 | 状态 | 说明 |
| --- | --- | --- |
| 技术参数完整 | 建议完善 | 1/2 个步骤参数齐全 |
| 步骤插图完整 | 建议完善 | 0/2 个步骤已配置插图 |
| 三维对象关联完整 | 建议完善 | 0/2 个步骤已关联模型对象 |
| 热点交互完整 | 建议完善 | 1/2 个步骤已启用热点 |
| 离线介质容量复核 | 建议完善 | 发布前由现场确认受控介质可用容量;不阻断 OFD 文件生成 |

## 八、需纳入验收的扩展交付

- OFD 加密、数字签名、长期归档与许可配置按甲方环境联调验收
- 作业指导书文字、规程与图表等内容素材制作
- 三维建模与标准件库建设

+ 36
- 0
reports/guide-closure-20260910/export.json 查看文件

@@ -0,0 +1,36 @@
{
"checks": [
{
"name": "导出 project 下载文件",
"status": "PASS",
"detail": ""
},
{
"name": "导出 requirement 下载文件",
"status": "PASS",
"detail": ""
},
{
"name": "导出 plan 下载文件",
"status": "PASS",
"detail": ""
},
{
"name": "导出 pdf 下载文件",
"status": "PASS",
"detail": ""
},
{
"name": "导出 ofd 下载文件",
"status": "PASS",
"detail": ""
},
{
"name": "导出 offline 下载文件",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
}

+ 21
- 0
reports/guide-closure-20260910/final-document.json 查看文件

@@ -0,0 +1,21 @@
{
"checks": [
{
"name": "狐狸按实际尺寸取景后保存",
"status": "PASS",
"detail": ""
},
{
"name": "卡车按实际尺寸取景后保存",
"status": "PASS",
"detail": ""
},
{
"name": "两个模型的取景图片重开后保留",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
}

二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


+ 50
- 0
reports/guide-closure-20260910/index.html
文件差異過大導致無法顯示
查看文件


+ 5
- 0
reports/guide-closure-20260910/inspect.json 查看文件

@@ -0,0 +1,5 @@
{
"checks": [],
"errors": [],
"requests": []
}

二進制
查看文件


+ 41
- 0
reports/guide-closure-20260910/layout.json 查看文件

@@ -0,0 +1,41 @@
{
"checks": [
{
"name": "插入表格、填写单元格、合并拆分与边框保存",
"status": "PASS",
"detail": ""
},
{
"name": "插入正文自由块后保存",
"status": "PASS",
"detail": ""
},
{
"name": "二维框标注保存",
"status": "PASS",
"detail": ""
},
{
"name": "动态阅读检索步骤",
"status": "PASS",
"detail": ""
},
{
"name": "表格、自由块、二维标注重开后保留",
"status": "PASS",
"detail": ""
},
{
"name": "列表按编号检索",
"status": "PASS",
"detail": ""
},
{
"name": "重置恢复创建时间排序",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
}

二進制
查看文件


二進制
查看文件


+ 56
- 0
reports/guide-closure-20260910/media.json 查看文件

@@ -0,0 +1,56 @@
{
"checks": [
{
"name": "模型插入步骤并保存关联",
"status": "PASS",
"detail": ""
},
{
"name": "关联导入模型的真实动画",
"status": "PASS",
"detail": ""
},
{
"name": "三维模型放大面板渲染画布",
"status": "PASS",
"detail": ""
},
{
"name": "步骤属性「三维」可打开",
"status": "PASS",
"detail": ""
},
{
"name": "步骤属性「动画」可打开",
"status": "PASS",
"detail": ""
},
{
"name": "步骤属性「标注」可打开",
"status": "PASS",
"detail": ""
},
{
"name": "步骤属性「公式」可打开",
"status": "PASS",
"detail": ""
},
{
"name": "步骤属性「版式」可打开",
"status": "PASS",
"detail": ""
},
{
"name": "步骤属性「内容」可打开",
"status": "PASS",
"detail": ""
},
{
"name": "media 脚本执行",
"status": "FAIL",
"detail": "locator.click: Error: strict mode violation: locator('[data-action=\"library-insert\"][data-kind=\"model\"]').filter({ hasText: 'MilkTruck' }) resolved to 3 elements:\n 1) <button title=\"挂到当前步骤\" draggable=\"true\" data-kind=\"model\" data-drag-kind=\"model\" data-action=\"library-insert\" class=\"ofd-model-tile active\" data-id=\"res-1788971891968-0\" data-drag-id=\"res-1788971891968-0\" data-resource-name=\"08-cesiummilktruck.glb\">…</button> aka getByRole('button', { name: '08-CesiumMilkTruck.glb 缩略图 08' }).first()\n 2) <button title=\"挂到当前步骤\" draggable=\"true\" data-kind=\"model\" data-drag-kind=\"model\" class=\"ofd-model-tile \" data-action=\"library-insert\" data-id=\"res-1788972081498-0\" data-drag-id=\"res-1788972081498-0\" data-resource-name=\"08-cesiummilktruck.glb\">…</button> aka getByRole('button', { name: '08-CesiumMilkTruck.glb 缩略图 08' }).nth(1)\n 3) <button title=\"挂到当前步骤\" draggable=\"true\" data-kind=\"model\" data-drag-kind=\"model\" class=\"ofd-model-tile \" data-action=\"library-insert\" data-id=\"res-1788972140795-0\" data-drag-id=\"res-1788972140795-0\" data-resource-name=\"08-cesiummilktruck.glb\">…</button> aka getByRole('button', { name: '08-CesiumMilkTruck.glb 缩略图 08' }).nth(2)\n\nCall log:\n\u001b[2m - waiting for locator('[data-action=\"library-insert\"][data-kind=\"model\"]').filter({ hasText: 'MilkTruck' })\u001b[22m\n"
}
],
"errors": [],
"requests": []
}

二進制
查看文件


+ 11
- 0
reports/guide-closure-20260910/missing.json 查看文件

@@ -0,0 +1,11 @@
{
"checks": [
{
"name": "访问不存在的指导书不会自动新建",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
}

二進制
查看文件


+ 41
- 0
reports/guide-closure-20260910/offline.json 查看文件

@@ -0,0 +1,41 @@
{
"checks": [
{
"name": "离线包章节步骤可以阅读与切换",
"status": "PASS",
"detail": ""
},
{
"name": "离线模型 1 动画可播放",
"status": "PASS",
"detail": ""
},
{
"name": "离线模型 1 观察操作可执行",
"status": "PASS",
"detail": ""
},
{
"name": "离线模型 2 动画可播放",
"status": "PASS",
"detail": ""
},
{
"name": "离线模型 2 观察操作可执行",
"status": "PASS",
"detail": ""
},
{
"name": "断开公网和业务服务后没有外部依赖请求",
"status": "PASS",
"detail": ""
},
{
"name": "离线个人批注与收藏入口",
"status": "FAIL",
"detail": "当前离线阅读器未提供原始需求中的个人笔记、收藏和学习进度回传"
}
],
"errors": [],
"requests": []
}

二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


二進制
查看文件


+ 26
- 0
reports/guide-closure-20260910/reader.json 查看文件

@@ -0,0 +1,26 @@
{
"checks": [
{
"name": "查看角色的正文编辑控件禁用",
"status": "PASS",
"detail": ""
},
{
"name": "查看角色可以使用导出菜单",
"status": "PASS",
"detail": ""
},
{
"name": "查看角色可以使用页码和缩放",
"status": "PASS",
"detail": ""
},
{
"name": "只读用户实际导出 OFD 且不修改工程",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
}

二進制
查看文件


二進制
查看文件


+ 31
- 0
reports/guide-closure-20260910/resources.json 查看文件

@@ -0,0 +1,31 @@
{
"checks": [
{
"name": "导入本地视频并在原生播放器实际播放",
"status": "PASS",
"detail": ""
},
{
"name": "视频当前帧海报保存到步骤",
"status": "PASS",
"detail": ""
},
{
"name": "非法工程 JSON 被拒绝且原工程保留",
"status": "PASS",
"detail": ""
},
{
"name": "编制工程同步到正式内容库",
"status": "FAIL",
"detail": "实测仅写入浏览器本地库;正式内容库查询 HTTP 200"
},
{
"name": "编制发布结果进入教学指导书列表",
"status": "FAIL",
"detail": "教学查询 HTTP 200"
}
],
"errors": [],
"requests": []
}

+ 46
- 0
reports/guide-closure-20260910/revision.json 查看文件

@@ -0,0 +1,46 @@
{
"checks": [
{
"name": "修订不改变旧的正式版本快照",
"status": "PASS",
"detail": ""
},
{
"name": "工程 JSON 导入恢复步骤与资源关联",
"status": "PASS",
"detail": ""
},
{
"name": "狐狸动画真实改变渲染帧",
"status": "PASS",
"detail": ""
},
{
"name": "狐狸截取当前角度保存纸面图片",
"status": "PASS",
"detail": ""
},
{
"name": "卡车动画真实改变渲染帧",
"status": "PASS",
"detail": ""
},
{
"name": "卡车截取当前角度保存纸面图片",
"status": "PASS",
"detail": ""
},
{
"name": "新修订再次发布并留存历史版本",
"status": "PASS",
"detail": ""
},
{
"name": "撤回本地正式版本",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
}

+ 905
- 0
reports/guide-closure-20260910/summary.json 查看文件

@@ -0,0 +1,905 @@
{
"date": "2026-09-10",
"conclusion": "未完全闭环;本地编辑与导出路径完成修复验证,正式内容、教学和对外 HTTP 接入待确认",
"environment": {
"web": "http://127.0.0.1:6180",
"gateway": 6100,
"browser": "Edge headless / Playwright",
"viewport": "1920×1080;390×844 观察",
"accounts": "root 编辑、admin 只读(不含凭据)"
},
"pass": 73,
"fail": 5,
"unitTests": 20,
"artifactChecks": [
{
"name": "PDF 四页可解析且每页含文本",
"status": "PASS",
"detail": "4 pages"
},
{
"name": "OFD ZIP 完整、入口和 XML 可解析",
"status": "PASS",
"detail": "9 XML files; 未使用第三方 OFD 阅读器认证"
},
{
"name": "离线 ZIP 完整",
"status": "PASS",
"detail": "16 files"
},
{
"name": "离线脚本没有 Vite 开发缓存引用",
"status": "PASS",
"detail": ""
},
{
"name": "离线狐狸和卡车 GLB 结构及动画保留",
"status": "PASS",
"detail": "导出按现有逻辑轻量化,卡车从 369980 缩至 318276 字节;非原文件逐字节复制。实际渲染和动画另由浏览器验证。"
},
{
"name": "离线包含本地 Three.js 和解码器",
"status": "PASS",
"detail": ""
}
],
"fixes": [
[
"不存在的工程地址自动新建",
"runtime.ts",
"不存在 ID 明确报错,返回列表选择,避免查看操作创建脏数据。",
"missing"
],
[
"列表默认排序及重置",
"GuideProjectList.vue / content-lists.js",
"统一按创建时间倒序;更新旧工程不应改变默认顺序。",
"templates / layout"
],
[
"只读用户无法选择导出和阅读控件",
"ofd-v4.js",
"放行导出格式、页码、缩放、搜索等阅读控件;OFD 导出不再尝试保存草稿。",
"reader"
],
[
"狐狸三维视口被环境雾完全遮住",
"ofd-viewport.js",
"单模型加载清除车间雾;加载场景仍使用场景自身环境配置。",
"revision / offline"
],
[
"默认模型相机距离不适配真实模型尺寸",
"ofd-document.js",
"缺省相机走自动取景,保留用户明确保存的相机位置。已有显式相机不批量覆盖。",
"final-document / regression"
],
[
"新建继承演示工程版本、反馈和装备参数",
"ofd-v4.js",
"新工程清空演示业务记录、零件、技术参数与来源文件;模板章节按原模板生成。",
"templates"
],
[
"开发环境导出的离线三维包加载失败",
"ofd-offline-runtime-assets.js",
"打包 Three.js 和解码器原始源码,消除 Vite 改写后的开发路径。",
"offline / artifact-validation"
],
[
"长标题、公式间距与动画秒数版式",
"ofd-layout.js",
"缺省长标题自适应,公式后增加必要间距,动画时间保留两位小数。",
"PDF 四页目视 / regression"
],
[
"反馈示例固定引用不存在的步骤",
"ofd-v4.js",
"默认反馈示例使用当前工程的实际步骤,避免新建工程执行示例直接 404。",
"api:submit-feedback 201"
]
],
"coverage": [
[
"列表",
"列表加载、编号搜索、重置、创建时间排序",
"本地通过",
"templates / layout",
"编辑旧记录后的多记录排序组合未单独量化。"
],
[
"列表",
"项目详情、状态、版本入口",
"部分覆盖",
"代码审查 + publish 截图",
"统计是本地数据,不能作为教学统计。"
],
[
"列表",
"删除及所有批量/权限组合",
"待补验证",
"代码审查",
"未删除用户工程;未对每一种授权组合做全矩阵测试。"
],
[
"创建",
"空白向导、基本信息、建档",
"本地通过",
"create",
"正式后台不存在该新工程。"
],
[
"创建",
"四类模板静态/动态预览",
"本地通过",
"templates",
"实际逐个打开并切换目录。"
],
[
"创建",
"模板 PDF、OFD 下载和选模板创建",
"本地通过",
"templates",
"下载实测第一类;新建实测第四类(4 章 9 步)。"
],
[
"创建",
"模板来源与平台模板管理联动",
"待确认改造",
"静态 OFD_CONTENT_TEMPLATES",
"当前不是后台模板库。"
],
[
"创建",
"演示历史、反馈、参数隔离",
"修复通过",
"templates",
"仅修复新建,不清理用户既有文档。"
],
[
"分析规划",
"资料来源、需求规格、构建计划导出",
"部分覆盖",
"02-planning / export",
"Markdown 下载已测;专业内容自动分析质量未验收。"
],
[
"分析规划",
"多装备、多层级、关键步骤覆盖",
"待业务验收",
"原始 DOCX 对照",
"狐狸、卡车为功能夹具,不能证明真实维修覆盖率。"
],
[
"编辑",
"步骤属性、保存、刷新重开",
"本地通过",
"edit",
"仅在同一测试浏览器项目空间持久化。"
],
[
"编辑",
"新步骤、续页、复制、撤销重做",
"本地通过",
"edit",
"章节及步骤所有拖动组合未穷举。"
],
[
"编辑",
"表格单元格、合并拆分、边框",
"本地通过",
"layout",
"填写后重开检查;未对所有样式排列组合做测试。"
],
[
"编辑",
"自由文本块、矩形标注",
"本地通过",
"layout",
"自由排版允许用户自行移动重叠;未自动约束所有自由布局。"
],
[
"编辑",
"公式输入、说明、关联区排版",
"修复通过",
"content / regression / PDF",
"两个公式回归与实际 PDF 检查。"
],
[
"编辑",
"字体、纸张、页眉页脚、所有标注形状",
"部分覆盖",
"属性面板巡检与源码",
"不是每个属性都做了写入、重开、导出三重验证。"
],
[
"资源",
"本地 GLB 导入、绑定步骤",
"本地通过",
"edit / revision",
"09-Fox.glb、08-CesiumMilkTruck.glb。"
],
[
"资源",
"后台模型制作/场景制作资源选择",
"待确认改造",
"本地 platformModels / 场景目录",
"不能声称使用了之前在服务端创建的两个场景。"
],
[
"资源",
"单模型显示、取景、截图、动画",
"修复通过",
"revision / final-document",
"实际图像和帧差验证,截图保存后重开保留。"
],
[
"资源",
"本地视频导入、播放、截帧保存",
"本地通过",
"resources",
"使用 2 秒测试视频;不代表真实维修内容验证。"
],
[
"资源",
"音频、资料包、损坏模型、存储配额",
"待补验证",
"导入代码审查",
"未对全部格式和存储异常做实际测试。"
],
[
"资源",
"模型热点、部件聚焦、三维与步骤语义关联",
"部分覆盖",
"属性面板 + 离线观察操作",
"专业热点与真实装备语义尚需专项验证。"
],
[
"预览",
"静态版式、动态步骤搜索与跳转",
"本地通过",
"layout / templates",
"在线预览位于本地编辑器。"
],
[
"预览",
"播放、暂停/重播、观察控制",
"本地通过",
"revision / offline",
"两模型动画帧改变;离线观察按钮逐个执行。"
],
[
"权限",
"管理员只读、导出不修改文档",
"修复通过",
"reader(实际 admin 会话)",
"编辑使用 root;未把三角色全接口授权矩阵当作已验证。"
],
[
"权限",
"账号间工程隔离、数据范围",
"待确认改造",
"共享本地存储命名空间",
"正式数据应以现有后台权限和用户范围为准。"
],
[
"发布",
"预检清单、草稿预览、发布面板",
"本地通过",
"08-preflight / 09-publish-panel",
"预检通过不等于后端发布成功。"
],
[
"发布",
"发布、修订、旧快照、新版本、撤回",
"本地通过",
"content / revision",
"现状均为本地生命周期;没有实际发送后台发布请求。"
],
[
"发布",
"后台版本、OFD/离线交付物原子发布",
"待确认改造",
"ContentProjectService",
"后台已要求 GUIDE 的 OFD + OFFLINE_HTML ZIP;现有页面未串联。"
],
[
"发布",
"审核策略",
"代码确认",
"ContentProjectService GUIDE 分支",
"审核按现有配置可选;不引入强制审核新规则。"
],
[
"导入导出",
"工程 JSON、导入恢复、非法 JSON",
"本地通过",
"revision / resources",
"JSON 不是完整资产备份;模型字节仍在 IndexedDB。"
],
[
"导入导出",
"实际 PDF 下载及四页版式",
"修复通过",
"final-pdf-page-1 至 4",
"中文、目录、模型图、公式目视检查;字体存在替代。"
],
[
"导入导出",
"实际 OFD ZIP/XML",
"本地通过",
"artifact-validation",
"未用第三方原生 OFD 阅读器验证标准一致性或签章。"
],
[
"导入导出",
"离线 ZIP、GLB 轻量化、运行时",
"修复通过",
"offline / artifact-validation",
"卡车导出按已有逻辑压缩;结构与动画保留,非原字节复制。"
],
[
"离线",
"断开公网/业务服务后本地启动阅读",
"本地通过",
"offline",
"Windows + Node.js 本地静态服务;没有依赖外站的请求。"
],
[
"离线",
"手机尺寸、麒麟、真实移动设备",
"部分覆盖",
"23-offline-mobile",
"390×844 可显示,按钮换行与三维取景仍偏拥挤;未测实机、麒麟。"
],
[
"教学",
"学员指导书列表、正式阅读内容",
"待确认改造",
"正式内容查询 + teaching 接口查询",
"不能从本地发布进入正式教学阅读。"
],
[
"教学",
"收藏、个人笔记、检索、进度",
"待确认改造",
"原始需求与离线入口核对",
"离线缺个人笔记/收藏入口;需与在线用户身份及进度方案一并实现。"
],
[
"教学",
"学习统计、报表、训练/考试联动",
"待确认改造",
"后端 teaching/guides 与原始需求",
"未因后台存在接口而判为全链路通过。"
],
[
"接口",
"九个本地契约操作与反馈样例",
"修复通过",
"api / revision",
"八个本地请求逐个执行;生命周期通过发布 UI 单独实测。"
],
[
"接口",
"第三方 HTTP 调用、鉴权、正式反馈",
"待确认改造",
"api:零真实业务 HTTP 请求",
"显示 /api/ofd 不代表服务端真实提供该接口。"
],
[
"质量",
"构建、单元回归、浏览器异常",
"通过",
"build 及 20 项测试",
"选定最终 E2E 阶段无 pageerror;不包含早期定位脚本失败。"
],
[
"质量",
"三主题、键盘、长文档、并发和性能指标",
"待补验证",
"本轮边界",
"未验证所有组合;不能宣称 <1s / ≥25fps / ≥99% / ≤10s 已达标。"
]
],
"proposals": [
[
"A:编辑与正式内容闭环(优先)",
"保留现有编辑器,将列表、工程保存、模板/模型/场景选择、权限和版本操作对接现有 content 接口。服务端为数据真值;明确迁移本地工程和 IndexedDB 文件,不能静默覆盖。发布时提交 OFD 和离线 ZIP,并遵循现有可选审核设置。",
"验收:新建→编辑→刷新/换浏览器→发布→旧版本不变→撤回;正式数据库与界面一致,其他账号按权限可见。"
],
[
"B:教学阅读闭环",
"衔接现有教学指导书、阅读记录和反馈接口;补齐个人笔记、收藏、学习进度与统计。离线笔记是否回传及冲突处理需要与在线范围一起定。",
"验收:教员发布→学员阅读→记录/笔记/反馈保存→教员统计可查询;账号隔离生效。"
],
[
"C:对外接口真实联调",
"将当前本地契约自检与真实 HTTP 接口对齐,文档标明真实路径、参数、错误及授权;沿用现有平台认证机制,不新增密钥文件或独立服务。",
"验收:从独立客户端调用,读取/修改/反馈真实工程,未授权请求被拒绝。"
]
],
"runs": [
{
"phase": "create",
"checks": [
{
"name": "通过界面从空白模板创建指导书",
"status": "PASS",
"detail": ""
},
{
"name": "创建调用正式内容接口",
"status": "FAIL",
"detail": "当前页面使用浏览器本地工程库"
},
{
"name": "新建工程出现在正式后端列表",
"status": "FAIL",
"detail": ""
}
],
"errors": [],
"requests": []
},
{
"phase": "edit",
"checks": [
{
"name": "步骤标题、说明、工具、安全、验收编辑保存",
"status": "PASS",
"detail": ""
},
{
"name": "刷新后恢复已保存的本地内容",
"status": "PASS",
"detail": ""
},
{
"name": "新增步骤后保存",
"status": "PASS",
"detail": ""
},
{
"name": "正文续页和复制页面保存",
"status": "PASS",
"detail": ""
},
{
"name": "撤销复制恢复页数",
"status": "PASS",
"detail": ""
},
{
"name": "重做复制恢复页数",
"status": "PASS",
"detail": ""
},
{
"name": "导入真实 Fox.glb 文件并登记本地资源",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
},
{
"phase": "reader",
"checks": [
{
"name": "查看角色的正文编辑控件禁用",
"status": "PASS",
"detail": ""
},
{
"name": "查看角色可以使用导出菜单",
"status": "PASS",
"detail": ""
},
{
"name": "查看角色可以使用页码和缩放",
"status": "PASS",
"detail": ""
},
{
"name": "只读用户实际导出 OFD 且不修改工程",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
},
{
"phase": "missing",
"checks": [
{
"name": "访问不存在的指导书不会自动新建",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
},
{
"phase": "content",
"checks": [
{
"name": "公式与说明保存",
"status": "PASS",
"detail": ""
},
{
"name": "发布保存本地正式版本",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
},
{
"phase": "revision",
"checks": [
{
"name": "修订不改变旧的正式版本快照",
"status": "PASS",
"detail": ""
},
{
"name": "工程 JSON 导入恢复步骤与资源关联",
"status": "PASS",
"detail": ""
},
{
"name": "狐狸动画真实改变渲染帧",
"status": "PASS",
"detail": ""
},
{
"name": "狐狸截取当前角度保存纸面图片",
"status": "PASS",
"detail": ""
},
{
"name": "卡车动画真实改变渲染帧",
"status": "PASS",
"detail": ""
},
{
"name": "卡车截取当前角度保存纸面图片",
"status": "PASS",
"detail": ""
},
{
"name": "新修订再次发布并留存历史版本",
"status": "PASS",
"detail": ""
},
{
"name": "撤回本地正式版本",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
},
{
"phase": "templates",
"checks": [
{
"name": "四类模板列表",
"status": "PASS",
"detail": ""
},
{
"name": "模板 1 静态版式预览",
"status": "PASS",
"detail": ""
},
{
"name": "模板 1 动态目录切换",
"status": "PASS",
"detail": ""
},
{
"name": "模板下载 pdf",
"status": "PASS",
"detail": ""
},
{
"name": "模板下载 ofd",
"status": "PASS",
"detail": ""
},
{
"name": "模板 2 静态版式预览",
"status": "PASS",
"detail": ""
},
{
"name": "模板 2 动态目录切换",
"status": "PASS",
"detail": ""
},
{
"name": "模板 3 静态版式预览",
"status": "PASS",
"detail": ""
},
{
"name": "模板 3 动态目录切换",
"status": "PASS",
"detail": ""
},
{
"name": "模板 4 静态版式预览",
"status": "PASS",
"detail": ""
},
{
"name": "模板 4 动态目录切换",
"status": "PASS",
"detail": ""
},
{
"name": "选用模板真正生成章节步骤",
"status": "PASS",
"detail": ""
},
{
"name": "新文档不混入演示版本和反馈",
"status": "PASS",
"detail": ""
},
{
"name": "新文档不混入其他装备参数与来源",
"status": "PASS",
"detail": ""
},
{
"name": "列表默认按创建时间倒序",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
},
{
"phase": "layout",
"checks": [
{
"name": "插入表格、填写单元格、合并拆分与边框保存",
"status": "PASS",
"detail": ""
},
{
"name": "插入正文自由块后保存",
"status": "PASS",
"detail": ""
},
{
"name": "二维框标注保存",
"status": "PASS",
"detail": ""
},
{
"name": "动态阅读检索步骤",
"status": "PASS",
"detail": ""
},
{
"name": "表格、自由块、二维标注重开后保留",
"status": "PASS",
"detail": ""
},
{
"name": "列表按编号检索",
"status": "PASS",
"detail": ""
},
{
"name": "重置恢复创建时间排序",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
},
{
"phase": "api",
"checks": [
{
"name": "对外接口展示九个操作",
"status": "PASS",
"detail": ""
},
{
"name": "本地接口调度 list-projects 返回成功",
"status": "PASS",
"detail": "200\nGET /api/ofd/projects\n10 ms\n{\n \"success\": true,\n \"data\": {\n \"total\": 3,"
},
{
"name": "本地接口调度 get-project 返回成功",
"status": "PASS",
"detail": "200\nGET /api/ofd/projects/OFD-mtub76ed-01\n8 ms\n{\n \"success\": true,\n \"data\": {\n"
},
{
"name": "本地接口调度 update-step 返回成功",
"status": "PASS",
"detail": "200\nPUT /api/ofd/projects/OFD-mtub76ed-01/steps/step-01\n26 ms\n{\n \"success\": tru"
},
{
"name": "本地接口调度 register-resource 返回成功",
"status": "PASS",
"detail": "201\nPOST /api/ofd/projects/OFD-mtub76ed-01/resources\n35 ms\n{\n \"success\": true,\n"
},
{
"name": "本地接口调度 collect-resources 返回成功",
"status": "PASS",
"detail": "201\nPOST /api/ofd/projects/OFD-mtub76ed-01/collect\n44 ms\n{\n \"success\": true,\n "
},
{
"name": "本地接口调度 inspect 返回成功",
"status": "PASS",
"detail": "200\nPOST /api/ofd/projects/OFD-mtub76ed-01/inspect\n9 ms\n{\n \"success\": true,\n \""
},
{
"name": "本地接口调度 delivery-manifest 返回成功",
"status": "PASS",
"detail": "200\nPOST /api/ofd/projects/OFD-mtub76ed-01/delivery-manifest\n6 ms\n{\n \"success\":"
},
{
"name": "本地接口调度 submit-feedback 返回成功",
"status": "PASS",
"detail": "201\nPOST /api/ofd/projects/OFD-mtub76ed-01/feedback\n25 ms\n{\n \"success\": true,\n "
},
{
"name": "接口面板未发送真实 HTTP 业务请求",
"status": "PASS",
"detail": "这里通过本地函数调度,需要接入正式 API 才可外部调用"
}
],
"errors": [],
"requests": []
},
{
"phase": "offline",
"checks": [
{
"name": "离线包章节步骤可以阅读与切换",
"status": "PASS",
"detail": ""
},
{
"name": "离线模型 1 动画可播放",
"status": "PASS",
"detail": ""
},
{
"name": "离线模型 1 观察操作可执行",
"status": "PASS",
"detail": ""
},
{
"name": "离线模型 2 动画可播放",
"status": "PASS",
"detail": ""
},
{
"name": "离线模型 2 观察操作可执行",
"status": "PASS",
"detail": ""
},
{
"name": "断开公网和业务服务后没有外部依赖请求",
"status": "PASS",
"detail": ""
},
{
"name": "离线个人批注与收藏入口",
"status": "FAIL",
"detail": "当前离线阅读器未提供原始需求中的个人笔记、收藏和学习进度回传"
}
],
"errors": [],
"requests": []
},
{
"phase": "final-document",
"checks": [
{
"name": "狐狸按实际尺寸取景后保存",
"status": "PASS",
"detail": ""
},
{
"name": "卡车按实际尺寸取景后保存",
"status": "PASS",
"detail": ""
},
{
"name": "两个模型的取景图片重开后保留",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
},
{
"phase": "export",
"checks": [
{
"name": "导出 project 下载文件",
"status": "PASS",
"detail": ""
},
{
"name": "导出 requirement 下载文件",
"status": "PASS",
"detail": ""
},
{
"name": "导出 plan 下载文件",
"status": "PASS",
"detail": ""
},
{
"name": "导出 pdf 下载文件",
"status": "PASS",
"detail": ""
},
{
"name": "导出 ofd 下载文件",
"status": "PASS",
"detail": ""
},
{
"name": "导出 offline 下载文件",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
},
{
"phase": "resources",
"checks": [
{
"name": "导入本地视频并在原生播放器实际播放",
"status": "PASS",
"detail": ""
},
{
"name": "视频当前帧海报保存到步骤",
"status": "PASS",
"detail": ""
},
{
"name": "非法工程 JSON 被拒绝且原工程保留",
"status": "PASS",
"detail": ""
},
{
"name": "编制工程同步到正式内容库",
"status": "FAIL",
"detail": "实测仅写入浏览器本地库;正式内容库查询 HTTP 200"
},
{
"name": "编制发布结果进入教学指导书列表",
"status": "FAIL",
"detail": "教学查询 HTTP 200"
}
],
"errors": [],
"requests": []
}
]
}

二進制
查看文件


二進制
查看文件


+ 81
- 0
reports/guide-closure-20260910/templates.json 查看文件

@@ -0,0 +1,81 @@
{
"checks": [
{
"name": "四类模板列表",
"status": "PASS",
"detail": ""
},
{
"name": "模板 1 静态版式预览",
"status": "PASS",
"detail": ""
},
{
"name": "模板 1 动态目录切换",
"status": "PASS",
"detail": ""
},
{
"name": "模板下载 pdf",
"status": "PASS",
"detail": ""
},
{
"name": "模板下载 ofd",
"status": "PASS",
"detail": ""
},
{
"name": "模板 2 静态版式预览",
"status": "PASS",
"detail": ""
},
{
"name": "模板 2 动态目录切换",
"status": "PASS",
"detail": ""
},
{
"name": "模板 3 静态版式预览",
"status": "PASS",
"detail": ""
},
{
"name": "模板 3 动态目录切换",
"status": "PASS",
"detail": ""
},
{
"name": "模板 4 静态版式预览",
"status": "PASS",
"detail": ""
},
{
"name": "模板 4 动态目录切换",
"status": "PASS",
"detail": ""
},
{
"name": "选用模板真正生成章节步骤",
"status": "PASS",
"detail": ""
},
{
"name": "新文档不混入演示版本和反馈",
"status": "PASS",
"detail": ""
},
{
"name": "新文档不混入其他装备参数与来源",
"status": "PASS",
"detail": ""
},
{
"name": "列表默认按创建时间倒序",
"status": "PASS",
"detail": ""
}
],
"errors": [],
"requests": []
}

二進制
查看文件


+ 5
- 0
reports/guide-closure-20260910/viewport-probe.json 查看文件

@@ -0,0 +1,5 @@
{
"checks": [],
"errors": [],
"requests": []
}

二進制
查看文件


二進制
查看文件


+ 39
- 0
tools/guide-artifact-check.py 查看文件

@@ -0,0 +1,39 @@
"""Validate real UI exports; run from ute2e. No authentication data is read."""
from pathlib import Path
import struct
import json
import zipfile
import xml.etree.ElementTree as ET
from pypdf import PdfReader

root = Path('reports/guide-closure-20260910')
checks = []
def check(name, passed, detail=''):
checks.append(dict(name=name, status='PASS' if passed else 'FAIL', detail=detail))

pdf = PdfReader(root / 'export-pdf.pdf')
check('PDF 四页可解析且每页含文本', len(pdf.pages) == 4 and all(p.extract_text().strip() for p in pdf.pages), f'{len(pdf.pages)} pages')
with zipfile.ZipFile(root / 'export-ofd.ofd') as z:
xmls = [n for n in z.namelist() if n.lower().endswith('.xml')]
for name in xmls:
ET.fromstring(z.read(name))
check('OFD ZIP 完整、入口和 XML 可解析', z.testzip() is None and 'OFD.xml' in z.namelist() and len(xmls) > 4, f'{len(xmls)} XML files; 未使用第三方 OFD 阅读器认证')
with zipfile.ZipFile(root / 'export-offline.zip') as z:
names = z.namelist()
check('离线 ZIP 完整', z.testzip() is None, f'{len(names)} files')
scripts = [z.read(n) for n in names if n.endswith(('.js', '.html'))]
check('离线脚本没有 Vite 开发缓存引用', not any(b'/.vite-cache/' in s or b'/node_modules/.pnpm/' in s for s in scripts))
glbs = [z.read(n) for n in names if n.lower().endswith('.glb')]
originals = [Path('../../文档/素材') / n for n in ['09-Fox.glb', '08-CesiumMilkTruck.glb']]
def structure(b):
assert b[:4] == b'glTF' and struct.unpack_from('<I', b, 8)[0] == len(b)
doc = json.loads(b[20:20 + struct.unpack_from('<I', b, 12)[0]])
return {k: len(doc.get(k, [])) for k in ['nodes', 'meshes', 'animations', 'materials', 'skins']}
expected = [structure(p.read_bytes()) for p in originals]
actual = [structure(b) for b in glbs]
check('离线狐狸和卡车 GLB 结构及动画保留', expected == actual, '导出按现有逻辑轻量化,卡车从 369980 缩至 318276 字节;非原文件逐字节复制。实际渲染和动画另由浏览器验证。')
check('离线包含本地 Three.js 和解码器', any('three.module' in n for n in names) and any(n.endswith('.wasm') for n in names))
(root / 'artifact-validation.json').write_text(json.dumps(dict(checks=checks), ensure_ascii=False, indent=2), encoding='utf-8')
for item in checks:
print(item['status'], item['name'])
raise SystemExit(any(c['status'] != 'PASS' for c in checks))

+ 510
- 0
tools/guide-closure-live.mjs 查看文件

@@ -0,0 +1,510 @@
import fs from 'node:fs'
import path from 'node:path'
import assert from 'node:assert/strict'
import { chromium, expect } from '@playwright/test'
import { login, call } from './role-permissions-live.mjs'

const report = path.resolve('reports/guide-closure-20260910')
const work = path.resolve('../../.codex-tmp/guide-closure-20260910')
fs.mkdirSync(report, { recursive:true }); fs.mkdirSync(work, { recursive:true })
const phase = process.argv[2] || 'create'
const fixtureFile = path.join(work, 'fixture.json')
const fixture = fs.existsSync(fixtureFile) ? JSON.parse(fs.readFileSync(fixtureFile, 'utf8')) : {}
const checks = [], errors = [], requests = []
const browser = await chromium.launch({ channel:'msedge', headless:true, args:['--enable-unsafe-swiftshader'] })
const session = await login(phase === 'reader' ? 'admin' : 'root')
const stateFile = path.join(work, 'browser.json')
const context = await browser.newContext({ viewport:{ width:1920, height:1080 }, ...(fs.existsSync(stateFile) ? { storageState:stateFile } : {}) })
await context.addInitScript(s => {
sessionStorage.setItem('unreal-tran:web:access-token:v1', s.accessToken)
sessionStorage.setItem('unreal-tran:web:refresh-token:v1', s.refreshToken)
localStorage.setItem('unreal-tran:web:preferences:v1', JSON.stringify({ themeMode:'military' }))
}, session)
const page = await context.newPage()
page.on('pageerror', e => errors.push(e.message))
page.on('request', r => { if (r.url().includes('/api/tran/')) requests.push({ method:r.method(), path:new URL(r.url()).pathname }) })
const check = (name, pass, detail='') => { checks.push({ name, status:pass ? 'PASS':'FAIL', detail }); console.log(`${pass ? 'PASS':'FAIL'} ${name}`) }
const shot = async name => { await page.screenshot({ path:path.join(report, `${name}.png`), animations:'disabled' }) }
const open = async url => { await page.goto(`http://127.0.0.1:6180${url}`); await expect(page.locator('.legacy-guide-page')).toBeVisible({ timeout:60000 }) }
const state = async () => page.evaluate(async id => (await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id)?.state, fixture.id)
try {
if (phase === 'create') {
await open('/content/ofd')
await page.getByRole('button', { name:'新建指导书', exact:true }).click()
await page.locator('[data-wizard="name"]').fill('闭环实测-狐狸巡检作业指导-20260910')
await page.locator('[data-wizard="code"]').fill('REVIEW-GUIDE-20260910')
await page.locator('[data-wizard="equipment"]').fill('狐狸模型测试装备')
await page.locator('[data-wizard="owner"]').fill('闭环验证')
await page.locator('[data-action="wizard-next"]').click()
await page.locator('[data-action="wizard-template"][data-template="__blank__"]').click()
await page.locator('[data-action="wizard-next"]').click()
await shot('01-create-wizard')
await page.locator('[data-action="wizard-next"]').click()
await page.waitForURL(/\/content\/ofd\/[^/]+\/studio\?view=analysis/)
fixture.id = page.url().match(/\/ofd\/([^/]+)\/studio/)[1]
fs.writeFileSync(fixtureFile, JSON.stringify(fixture))
await expect(page.locator('.ofd-view-analysis')).toBeVisible()
const saved = await state()
check('通过界面从空白模板创建指导书', saved?.project?.name.includes('闭环实测') && saved.chapters.length === 1)
check('创建调用正式内容接口', requests.some(r=>r.method==='POST' && r.path.endsWith('/content/projects')), '当前页面使用浏览器本地工程库')
const server = await call(`tran/v1/content/projects?type=GUIDE&keyword=${encodeURIComponent('闭环实测-狐狸巡检作业指导-20260910')}`, session.accessToken)
check('新建工程出现在正式后端列表', server.status===200 && server.data.records.some(r=>r.name.includes('闭环实测-狐狸')))
await shot('02-planning')
await page.locator('[data-action="goto"][data-view="editor"]').first().click()
await expect(page.locator('.ofd-view-editor')).toBeVisible()
await shot('03-editor-baseline')
}
if (phase === 'inspect') {
await open(`/content/ofd/${fixture.id}/studio?view=editor`)
await expect(page.locator('.ofd-view-editor')).toBeVisible()
const dom = await page.locator('.ofd-app').evaluate(el=>({
actions:[...el.querySelectorAll('[data-action]')].map(x=>({action:x.dataset.action,text:(x.innerText||x.getAttribute('title')||'').slice(0,60),view:x.dataset.view,field:x.dataset.field})),
fields:[...el.querySelectorAll('input,textarea,select')].map(x=>({field:x.dataset.field,stepField:x.dataset.stepField,projectField:x.dataset.projectField,placeholder:x.getAttribute('placeholder'),type:x.type,value:x.type==='password'?'':x.value.slice(0,80)})),
}))
fs.writeFileSync(path.join(work,'dom.json'),JSON.stringify(dom,null,2))
console.log(JSON.stringify(dom,null,2))
}
if (phase === 'edit') {
await open(`/content/ofd/${fixture.id}/studio?view=editor`)
const first = (await state()).chapters[0].steps[0].id
fixture.firstStep = first
for (const [field,value] of Object.entries({title:'狐狸巡检目标识别',summary:'定位狐狸模型,观察目标外观,并逐项记录本次巡检结果。',tools:'测试模型、观察记录表',safety:'仅使用测试模型,不作为真实装备维修规程。',acceptance:'模型显示完整,能够识别并记录目标。'})) {
await page.locator(`[data-step-field="${field}"]`).fill(value)
await page.locator(`[data-step-field="${field}"]`).blur()
}
await page.locator('[data-action="save"]').click()
await expect.poll(async()=> (await state()).chapters[0].steps[0].title).toBe('狐狸巡检目标识别')
check('步骤标题、说明、工具、安全、验收编辑保存', (await state()).chapters[0].steps[0].summary.includes('逐项记录'))
await page.reload(); await expect(page.locator('[data-step-field="title"]')).toHaveValue('狐狸巡检目标识别')
check('刷新后恢复已保存的本地内容', true)
const before = (await state()).chapters.flatMap(c=>c.steps).length
await page.locator('[data-action="add-step"]').click()
await page.locator('[data-step-field="title"]').fill('牛奶卡车状态检查')
await page.locator('[data-step-field="title"]').blur()
await page.locator('[data-action="save"]').click()
check('新增步骤后保存', (await state()).chapters.flatMap(c=>c.steps).length===before+1)
await page.locator('[data-action="page-add-for-step"]').last().click()
await page.locator('[data-action="save"]').click()
const pages=(await state()).documentPages.length
await page.locator('[data-action="page-duplicate"]').click()
await page.locator('[data-action="save"]').click()
check('正文续页和复制页面保存', (await state()).documentPages.length===pages+1)
await page.locator('[data-action="undo"]').click(); await page.locator('[data-action="save"]').click()
check('撤销复制恢复页数', (await state()).documentPages.length===pages)
await page.locator('[data-action="redo"]').click(); await page.locator('[data-action="save"]').click()
check('重做复制恢复页数', (await state()).documentPages.length===pages+1)
await shot('04-edit-pages')
for (const tab of ['three','motion','annotation','formula','layout']) {
const button=page.locator(`[data-action="property-tab"][data-property-tab="${tab}"]`)
if (await button.count()) { await button.click(); check(`属性面板 ${tab} 打开`,true) }
}
await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材/09-Fox.glb'))
await expect.poll(async()=> (await state()).resources.some(r=>r.name.includes('Fox')), {timeout:30000}).toBe(true)
check('导入真实 Fox.glb 文件并登记本地资源',true)
await page.locator('[data-action="right-dock-tab"]').filter({hasText:'资源'}).click()
await shot('05-import-fox')
const actions=await page.locator('.ofd-app').evaluate(el=>[...el.querySelectorAll('[data-action]')].map(x=>({action:x.dataset.action,text:(x.innerText||x.title||'').slice(0,50),data:{...x.dataset}})))
fs.writeFileSync(path.join(work,'resource-actions.json'),JSON.stringify(actions,null,2))
fs.writeFileSync(fixtureFile,JSON.stringify(fixture))
}
if (phase === 'reader') {
await open(`/content/ofd/${fixture.id}/studio?view=editor`)
await expect(page.locator('.ofd-view-editor')).toBeVisible()
check('查看角色的正文编辑控件禁用',await page.locator('[data-step-field="title"]').isDisabled())
check('查看角色可以使用导出菜单',await page.locator('[data-field="export-target"]').isEnabled())
check('查看角色可以使用页码和缩放',await page.locator('[data-field="canvas-page"]').isEnabled() && await page.locator('[data-field="canvas-zoom-choice"]').isEnabled())
const before=JSON.stringify(await state())
const download=page.waitForEvent('download')
await page.locator('[data-field="export-target"]').selectOption('ofd')
await (await download).saveAs(path.join(report,'reader-export.ofd'))
await page.waitForTimeout(700)
check('只读用户实际导出 OFD 且不修改工程',before===JSON.stringify(await state()))
await shot('reader-before')
}
if (phase === 'missing') {
const unknown='REVIEW-NOT-EXIST-20260910'
await open(`/content/ofd/${unknown}/studio?view=editor`)
await page.waitForTimeout(1200)
const exists=await page.evaluate(async id=>Boolean((await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id)),unknown)
check('访问不存在的指导书不会自动新建',!exists)
await shot('missing-before')
if(exists) await page.evaluate(async id=>(await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.deleteOFDProject(id),unknown)
}
if (phase === 'media') {
await open(`/content/ofd/${fixture.id}/studio?view=editor`)
// Playwright storageState does not preserve Blob prototypes across contexts.
// Re-import real bytes, then test reload inside this context below.
await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材/09-Fox.glb'))
await page.waitForTimeout(1000)
await page.reload()
await expect(page.locator('.ofd-view-editor')).toBeVisible()
await page.locator('[data-action="select-step"]').first().click()
await page.locator('[data-action="right-dock-tab"]').filter({hasText:'资源'}).click()
await page.locator('[data-action="library-insert"][data-kind="model"]').filter({hasText:'09-Fox.glb'}).first().click()
await page.locator('[data-action="save"]').click()
check('模型插入步骤并保存关联', (await state()).chapters[0].steps[0].mediaBlocks?.some(b=>b.assetId))
await page.locator('[data-action="right-dock-tab"]').filter({hasText:'资源'}).click()
await page.locator('[data-action="library-insert"][data-kind="animation"]').filter({hasText:'Run'}).first().click()
await page.locator('[data-action="save"]').click()
check('关联导入模型的真实动画', JSON.stringify((await state()).chapters[0].steps[0]).includes('Run'))
await page.locator('[data-action="media-open"]').first().click()
await expect(page.locator('.ofd-media-panel canvas').first()).toBeVisible({timeout:30000})
await expect(page.locator('.ofd-media-panel')).not.toContainText('模型暂不可用', {timeout:30000})
await page.waitForTimeout(2500)
await shot('06-fox-model-panel')
await page.locator('[data-action="media-view"][data-view="all"]').click()
await page.waitForTimeout(1500)
await shot('06b-fox-framed')
const controls=await page.locator('.ofd-media-panel').evaluate(el=>[...el.querySelectorAll('[data-action]')].map(x=>({action:x.dataset.action,text:(x.innerText||x.title||'').slice(0,60),data:{...x.dataset}})))
fs.writeFileSync(path.join(work,'media-controls.json'),JSON.stringify(controls,null,2))
check('三维模型放大面板渲染画布',true)
await page.locator('[data-action="media-close"]').click()
await page.locator('[data-action="right-dock-tab"]').filter({hasText:'属性'}).click()
for (const [text, tab] of Object.entries({'三维':'model','动画':'animation','标注':'annotation','公式':'formula','版式':'layout','内容':'content'})) {
await page.locator(`[data-action="property-tab"][data-tab="${tab}"]`).click()
check(`步骤属性「${text}」可打开`,true)
await shot(`property-${text}`)
}
await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材/08-CesiumMilkTruck.glb'))
await expect.poll(async()=> (await state()).resources.some(r=>r.name.includes('MilkTruck')), {timeout:30000}).toBe(true)
await page.locator('[data-action="select-step"]').last().click()
await page.locator('[data-action="right-dock-tab"]').filter({hasText:'资源'}).click()
await page.locator('[data-action="library-insert"][data-kind="model"]').filter({hasText:'MilkTruck'}).first().click()
await page.locator('[data-action="save"]').click()
check('第二步骤绑定牛奶卡车模型',JSON.stringify((await state()).chapters.flatMap(c=>c.steps).at(-1)).includes('MilkTruck'))
await shot('07-milk-truck-step')
}
if (phase === 'export') {
await open(`/content/ofd/${fixture.id}/studio?view=editor`)
for (const file of ['09-Fox.glb','08-CesiumMilkTruck.glb']) {
await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材',file))
await page.waitForTimeout(900)
}
for (const target of ['project','requirement','plan','pdf','ofd','offline']) {
try {
const waiting=page.waitForEvent('download',{timeout:90000})
await page.locator('[data-field="export-target"]').selectOption(target)
const download=await waiting
const file=path.join(report, `export-${target}${path.extname(download.suggestedFilename())}`)
await download.saveAs(file)
check(`导出 ${target} 下载文件`,fs.statSync(file).size>0)
if(target==='project') fixture.exportFile=file
} catch(error) { check(`导出 ${target}`,false,error.message); await shot(`export-${target}-failure`) }
}
fs.writeFileSync(fixtureFile,JSON.stringify(fixture))
await page.locator('[data-action="open-health"]').click()
await shot('08-preflight')
const details=await page.locator('.ofd-app').innerText()
fs.writeFileSync(path.join(work,'preflight.txt'),details)
await page.locator('[data-action="close-workspace-dialog"].ofd-icon-btn').click()
await page.locator('[data-action="open-publish"]').click()
await shot('09-publish-panel')
const actions=await page.locator('.ofd-app').evaluate(el=>[...el.querySelectorAll('[data-action]')].map(x=>({action:x.dataset.action,text:(x.innerText||x.title||'').slice(0,60),data:{...x.dataset}})))
fs.writeFileSync(path.join(work,'publish-actions.json'),JSON.stringify(actions,null,2))
}
if (phase === 'content') {
await open(`/content/ofd/${fixture.id}/studio?view=editor`)
await page.locator('[data-action="select-step"]').first().click()
await page.locator('[data-action="right-dock-tab"]').filter({hasText:'属性'}).click()
await page.locator('[data-action="property-tab"][data-tab="formula"]').click()
await page.locator('[data-action="formula-add"]').click()
await page.locator('[data-formula-field="source"]').last().fill('F = p \\times A')
await page.locator('[data-formula-field="source"]').last().blur()
await page.locator('[data-formula-field="caption"]').last().fill('闭环验证公式,仅用于排版测试')
await page.locator('[data-formula-field="caption"]').last().blur()
await page.locator('[data-action="save"]').click()
check('公式与说明保存', (await state()).chapters[0].steps[0].formulas.some(f=>f.caption.includes('闭环验证')))
await shot('10-formula')
await page.locator('[data-action="ribbon-tab"]').filter({hasText:'插入'}).click()
fs.writeFileSync(path.join(work,'insert-dom.json'), JSON.stringify(await page.locator('.ofd-app').evaluate(el=>({actions:[...el.querySelectorAll('[data-action]')].map(x=>({...x.dataset,text:(x.innerText||x.title||'').slice(0,50)})),inputs:[...el.querySelectorAll('input,select,textarea')].map(x=>({id:x.id,...x.dataset,type:x.type,value:x.value.slice(0,40)}))})),null,2))
await shot('11-insert-tools')
await page.locator('[data-action="open-publish"]').click()
await shot('09-publish-panel')
await page.locator('[data-action="lifecycle-action"][data-lifecycle-action="publish"]').click()
await expect.poll(async()=>(await state()).lifecycle.status).toBe('published')
check('发布保存本地正式版本',true)
fixture.publishedTitle=(await state()).chapters[0].steps[0].title
await shot('12-published')
fs.writeFileSync(path.join(work,'published-state.json'),JSON.stringify(await state()))
fs.writeFileSync(fixtureFile,JSON.stringify(fixture))
}
if (phase === 'revision') {
await open(`/content/ofd/${fixture.id}/studio?view=publish`)
await page.locator('[data-action="lifecycle-action"][data-lifecycle-action="edit"]').click()
await expect.poll(async()=>(await state()).lifecycle.status).toBe('draft')
await page.locator('[data-action="goto"][data-view="editor"]').click()
await page.locator('[data-action="select-step"]').first().click()
await page.locator('[data-action="right-dock-tab"]').filter({hasText:'属性'}).click()
await page.locator('[data-action="property-tab"][data-tab="content"]').click()
await page.locator('[data-step-field="title"]').fill('狐狸巡检目标识别(修订)')
await page.locator('[data-step-field="title"]').blur()
await page.locator('[data-action="save"]').click()
const edited=await state()
check('修订不改变旧的正式版本快照',edited.lifecycle.publishedSnapshot.chapters[0].steps[0].title===fixture.publishedTitle && edited.chapters[0].steps[0].title!==fixture.publishedTitle)
// 清理由重复运行测试产生的重叠媒体块,通过用户可用的 JSON 导入入口回灌。
const bundle={schema:'',state:edited}
Object.assign(bundle,JSON.parse(fs.readFileSync(path.join(report,'export-project.json'),'utf8')),{state:edited})
for (const c of edited.chapters) for (const s of c.steps) {
s.mediaBlocks=(s.mediaBlocks||[]).filter(b=>b.kind==='model').slice(-1)
for (const b of s.mediaBlocks) { b.poster='';b.posterAssetSignature='';b.camera=null }
}
const cleanFile=path.join(work,'roundtrip.json');fs.writeFileSync(cleanFile,JSON.stringify(bundle))
await page.locator('#ofd-project-file').setInputFiles(cleanFile)
await expect.poll(async()=>(await state()).chapters[0].steps[0].mediaBlocks.length).toBe(1)
check('工程 JSON 导入恢复步骤与资源关联',true)
for (const file of ['09-Fox.glb','08-CesiumMilkTruck.glb']) {
await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材',file));await page.waitForTimeout(1200)
}
await page.reload();await expect(page.locator('.ofd-view-editor')).toBeVisible()
for (let i=0;i<2;i++) {
await page.locator('[data-action="select-step"]').nth(i).click()
const b=(await state()).chapters[0].steps[i].mediaBlocks[0]
await page.locator(`[data-action="canvas-block"][data-block-id="step:${(await state()).chapters[0].steps[i].id}:media:${b.id}"]`).click()
await page.locator('[data-action="media-open"]').first().click()
await expect(page.locator('.ofd-media-panel canvas')).toBeVisible()
await page.waitForTimeout(1300)
await shot(i?'14-truck-panel':'13-fox-panel')
const before=await page.locator('.ofd-media-panel canvas').screenshot()
await page.locator('[data-action="media-play"]').click()
await page.waitForTimeout(380)
const after=await page.locator('.ofd-media-panel canvas').screenshot()
check(`${i?'卡车':'狐狸'}动画真实改变渲染帧`,!before.equals(after))
await page.locator('[data-action="media-apply"]').click()
await page.locator('[data-action="save"]').click()
check(`${i?'卡车':'狐狸'}截取当前角度保存纸面图片`,(await state()).chapters[0].steps[i].mediaBlocks[0].poster.startsWith('data:image/jpeg'))
}
await shot('15-clean-editor')
await page.locator('[data-action="open-publish"]').click()
await page.locator('[data-publish="note"]').fill('狐狸与牛奶卡车模型闭环验证')
await page.locator('[data-publish="note"]').blur()
await page.locator('[data-action="lifecycle-action"][data-lifecycle-action="publish"]').click()
await expect.poll(async()=>(await state()).lifecycle.status).toBe('published')
check('新修订再次发布并留存历史版本',(await state()).versions.filter(v=>v.immutable).length>=2)
await page.locator('[data-action="lifecycle-action"][data-lifecycle-action="withdraw"]').click()
await expect.poll(async()=>(await state()).lifecycle.status).toBe('withdrawn')
check('撤回本地正式版本',true)
await shot('16-withdrawn')
}
if (phase === 'templates') {
await open('/content/ofd/templates')
const ids=await page.locator('[data-guide-tpl-action="select"]').evaluateAll(els=>els.map(e=>e.dataset.guideTplId))
check('四类模板列表',ids.length===4)
for (let i=0;i<ids.length;i++) {
const id=ids[i]
await page.locator(`[data-guide-tpl-action="preview"][data-guide-tpl-id="${id}"]`).click()
await expect(page.locator('.guide-tpl-ofd-pages')).toBeVisible()
check(`模板 ${i+1} 静态版式预览`,true)
await page.locator('[data-guide-tpl-mode="interactive"]').click()
await page.locator('[data-guide-tpl-action="preview-step"]').last().click()
check(`模板 ${i+1} 动态目录切换`,await page.locator('[data-guide-tpl-action="preview-step"]').last().evaluate(e=>e.classList.contains('active')))
if (i===0) {
await shot('17-template-preview')
for (const format of ['pdf','ofd']) {
const wait=page.waitForEvent('download');await page.locator(`[data-guide-tpl-action="download"][data-guide-tpl-format="${format}"]`).click()
await (await wait).saveAs(path.join(report,`template.${format}`));check(`模板下载 ${format}`,true)
}
}
await page.locator('[data-guide-tpl-action="close-preview"][aria-label="关闭"]').click()
}
await page.locator('[data-guide-tpl-action="create"]').click()
await page.locator('[data-wizard="name"]').fill('闭环实测-牛奶卡车指导书-20260910')
await page.locator('[data-wizard="code"]').fill('REVIEW-GUIDE-TRUCK-20260910')
await page.locator('[data-wizard="equipment"]').fill('牛奶卡车测试模型')
await page.locator('[data-action="wizard-next"]').click()
await page.locator('[data-action="wizard-next"]').click()
await page.waitForURL(/view=analysis/)
fixture.templateId=page.url().match(/\/ofd\/([^/]+)\/studio/)[1]
const created=await page.evaluate(async id=>(await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id).state,fixture.templateId)
check('选用模板真正生成章节步骤',created.project.templateId===ids.at(-1) && created.chapters.length===4 && created.chapters.flatMap(c=>c.steps).length===9)
check('新文档不混入演示版本和反馈',!created.versions.length && !created.feedback.length && !created.lifecycle.audit.length)
check('新文档不混入其他装备参数与来源',!created.project.sourcePackage && !created.partsCatalog.length && !created.technicalParameters.length)
await shot('18-new-template-guide')
fs.writeFileSync(fixtureFile,JSON.stringify(fixture))
await open('/content/ofd')
await expect(page.locator('.el-table__body-wrapper tbody tr').first()).toContainText('牛奶卡车指导书')
check('列表默认按创建时间倒序',true)
await shot('19-list-created-order')
}
if (phase === 'api') {
await open('/content/ofd/api')
await page.locator('[data-field="api-project"]').selectOption(fixture.id)
const ids=await page.locator('[data-action="api-endpoint"]').evaluateAll(es=>es.map(e=>e.dataset.id))
check('对外接口展示九个操作',ids.length===9)
// 写操作只操作隔离浏览器中的本轮测试文档,生命周期已单独实测。
for (const id of ids) {
await page.locator(`[data-action="api-endpoint"][data-id="${id}"]`).click()
if (/lifecycle|workflow|step-update|resource-register/.test(id)) continue
await page.locator('[data-action="api-run"]').click()
await page.waitForTimeout(300)
const response=await page.locator('.ofd-api-result').innerText()
fs.writeFileSync(path.join(work,`api-${id}.txt`),response)
check(`本地接口调度 ${id} 返回成功`,/^20\d/.test(response),response.slice(0,80))
}
check('接口面板未发送真实 HTTP 业务请求',requests.length===0,'这里通过本地函数调度,需要接入正式 API 才可外部调用')
await shot('20-api-local')
}
if (phase === 'offline') {
const external=[]
page.on('console',msg=>{if(msg.type()==='error') console.log('BROWSER',msg.text())})
page.on('requestfailed',r=>console.log('NETWORK',r.url(),r.failure()))
await context.route('**/*',r=>{
if (new URL(r.request().url()).host==='127.0.0.1:18766') return r.continue()
external.push(r.request().url());return r.abort()
})
await page.goto('http://127.0.0.1:18766')
await page.locator('[data-action="start"]').click()
await page.locator('[data-action="next"]').click()
check('离线包章节步骤可以阅读与切换',await page.locator('.reader-panel:visible').count()>0)
await shot('21-offline-reading')
await page.locator('[data-action="open-model-library"]').click()
console.log('OFFLINE DOM',await page.evaluate(()=>({scripts:[...document.scripts].map(s=>({type:s.type,src:s.src,size:s.textContent.length})),status:document.querySelector('[data-model-status]')?.textContent})))
await expect(page.locator('[data-model-stage] canvas')).toBeVisible({timeout:30000})
await expect(page.locator('[data-model-status]')).toContainText(/个网格/, {timeout:30000})
for (let i=0;i<2;i++) {
await page.locator('[data-model-select]').selectOption(String(i))
await page.waitForTimeout(1400)
await shot(`22-offline-model-${i}`)
const before=await page.locator('[data-model-stage] canvas').screenshot()
await page.locator('[data-model-action="play-animation"]').click();await page.waitForTimeout(500)
const after=await page.locator('[data-model-stage] canvas').screenshot()
check(`离线模型 ${i+1} 动画可播放`,!before.equals(after))
await page.locator('[data-model-action="pause-animation"]').click()
for (const action of ['zoom-in','zoom-out','rotate','toggle-transparent','highlight','explode','reset']) await page.locator(`[data-model-action="${action}"]`).click()
check(`离线模型 ${i+1} 观察操作可执行`,true)
}
check('断开公网和业务服务后没有外部依赖请求',external.length===0,external.join('\n'))
check('离线个人批注与收藏入口',await page.getByText(/收藏|添加笔记|保存笔记/).count()>0,'当前离线阅读器未提供原始需求中的个人笔记、收藏和学习进度回传')
await page.setViewportSize({width:390,height:844});await shot('23-offline-mobile')
}
if (phase === 'layout') {
await open(`/content/ofd/${fixture.templateId}/studio?view=editor`)
await page.waitForTimeout(700)
console.log('LOAD',await page.locator('.el-message').allTextContents(),await page.locator('.legacy-guide-page').innerText().then(t=>t.slice(0,100)))
const get=async()=>page.evaluate(async id=>(await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id).state,fixture.templateId)
await page.locator('[data-action="select-step"]:visible').first().click()
await page.locator('[data-action="ribbon-tab"][data-tab="insert"]').click()
await page.locator('[data-field="canvas-insert"]').selectOption('table')
await page.locator('[data-action="table-cell"]').first().dblclick()
await page.locator('[data-inline-field="cell"]').fill('检测项目')
await page.locator('[data-inline-field="cell"]').blur()
await page.locator('[data-action="ribbon-tab"][data-tab="home"]').click()
await page.locator('[data-action="table-op"][data-op="merge-selection"]').click()
await page.locator('[data-action="table-op"][data-op="split-cell"]').click()
await page.locator('[data-field="table-border-style"]').selectOption('dashed')
await page.locator('[data-action="table-op"][data-op="border-apply"]').click()
await page.locator('[data-action="save"]').click()
check('插入表格、填写单元格、合并拆分与边框保存',JSON.stringify(await get()).includes('检测项目'))
await shot('24-table-edit')
await page.locator('[data-action="ribbon-tab"][data-tab="insert"]').click()
await page.locator('[data-field="canvas-insert"]').selectOption('text')
await page.locator('[data-action="save"]').click()
const stored=JSON.stringify(await get())
check('插入正文自由块后保存',stored.includes('extraBlocks'))
await page.locator('[data-action="ribbon-tab"][data-tab="review"]').click()
await page.locator('[data-action="canvas-tool"][data-tool="rect"]').click()
const canvas=await page.locator('[data-action="annotate-canvas"]').boundingBox()
await page.mouse.move(canvas.x+170,canvas.y+150)
await page.mouse.down();await page.mouse.move(canvas.x+280,canvas.y+220,{steps:8});await page.mouse.up()
await page.locator('[data-action="save"]').click()
check('二维框标注保存',JSON.stringify(await get()).includes('markups'))
await shot('25-markup')
await page.locator('[data-action="canvas-tab"][data-tab="interactive"]').click()
await page.locator('[data-field="interactive-search"]').fill('牛奶卡车')
await expect(page.locator('.ofd-ia-search-results')).toBeVisible()
check('动态阅读检索步骤',true)
await page.locator('[data-action="interactive-search-clear"]').click()
await page.locator('[data-action="interactive-next"]').click()
await shot('26-interactive-search')
await page.reload();await expect(page.locator('.ofd-view-editor')).toBeVisible()
check('表格、自由块、二维标注重开后保留',JSON.stringify(await get()).includes('检测项目') && JSON.stringify(await get()).includes('markups'))
await open('/content/ofd')
await page.getByPlaceholder('搜索指导书标题、编号或作者').fill('REVIEW-GUIDE-TRUCK-20260910')
await page.getByRole('button',{name:'搜索',exact:true}).click()
check('列表按编号检索',await page.locator('.el-table__body-wrapper tbody tr').count()>=1)
await page.getByRole('button',{name:'重置',exact:true}).click()
check('重置恢复创建时间排序',await page.getByRole('combobox',{name:'指导书排序'}).getAttribute('aria-label')==='指导书排序')
}
if (phase === 'final-document') {
await open(`/content/ofd/${fixture.id}/studio?view=editor`)
const current=await state()
const bundle=JSON.parse(fs.readFileSync(path.join(report,'export-project.json'),'utf8'));bundle.state=current
current.documentPages=current.documentPages.filter(p=>p.contentMode!=='continuation')
for (const c of current.chapters) for (const [i,s] of c.steps.entries()) {
s.image='';s.modelRef.camera=null
s.summary=i?'确认牛奶卡车测试模型完整显示,播放车轮动画并记录测试结果。':s.summary
s.safety='测试素材仅用于功能验证。';s.acceptance='模型与动画可查看,记录保存后能够重新打开。'
for (const b of s.mediaBlocks) { b.camera=null;b.poster='';b.posterAssetSignature='' }
}
const file=path.join(work,'final-document.json');fs.writeFileSync(file,JSON.stringify(bundle))
await page.locator('#ofd-project-file').setInputFiles(file)
await page.waitForTimeout(800)
for (const file of ['09-Fox.glb','08-CesiumMilkTruck.glb']) {
await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材',file));await page.waitForTimeout(1000)
}
await page.reload();await expect(page.locator('.ofd-view-editor')).toBeVisible()
for (let i=0;i<2;i++) {
await page.locator('[data-action="select-step"]').nth(i).click()
const s=(await state()).chapters[0].steps[i],b=s.mediaBlocks[0]
await page.locator(`[data-action="canvas-block"][data-block-id="step:${s.id}:media:${b.id}"]`).click()
await page.locator('[data-action="media-open"]').first().click();await page.waitForTimeout(1300)
await page.locator('[data-action="media-view"][data-view="all"]').click();await page.waitForTimeout(250)
await shot(i?'28-final-truck':'27-final-fox')
await page.locator('[data-action="media-apply"]').click();await page.locator('[data-action="save"]').click()
check(`${i?'卡车':'狐狸'}按实际尺寸取景后保存`,(await state()).chapters[0].steps[i].mediaBlocks[0].poster.startsWith('data:image/jpeg'))
}
await page.reload();await expect(page.locator('.ofd-view-editor')).toBeVisible()
check('两个模型的取景图片重开后保留',(await state()).chapters[0].steps.every(s=>s.mediaBlocks[0].poster.startsWith('data:image/jpeg')))
}
if (phase === 'resources') {
await open(`/content/ofd/${fixture.templateId}/studio?view=editor`)
await page.locator('[data-action="select-step"]:visible').first().click()
await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../.codex-tmp/guide-test-video.mp4'))
await page.waitForTimeout(700)
await page.locator('[data-action="right-dock-tab"]').filter({hasText:'资源'}).click()
await page.locator('[data-action="library-insert"][data-kind="resource"]').filter({hasText:'guide-test-video'}).first().click()
await page.locator('[data-action="media-open"]').first().click()
const video=page.locator('[data-media-video]')
await expect.poll(()=>video.evaluate(v=>v.readyState)).toBeGreaterThanOrEqual(2)
const start=await video.evaluate(v=>{v.play();return v.currentTime})
await page.waitForTimeout(450)
check('导入本地视频并在原生播放器实际播放',await video.evaluate(v=>v.currentTime)>start)
await page.locator('[data-action="video-capture-poster"]').click()
await shot('29-video-poster')
await page.locator('[data-action="media-close"]').click()
await page.locator('[data-action="save"]').click()
const doc=await page.evaluate(async id=>(await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id).state,fixture.templateId)
check('视频当前帧海报保存到步骤',doc.chapters.flatMap(c=>c.steps).some(s=>s.mediaBlocks.some(b=>b.kind==='video' && b.poster.startsWith('data:image/jpeg'))))
const original=JSON.stringify(doc)
await page.locator('#ofd-project-file').setInputFiles({name:'broken.json',mimeType:'application/json',buffer:Buffer.from('{broken')})
await page.waitForTimeout(500)
const after=await page.evaluate(async id=>(await import('/src/features/guide-legacy/runtime.ts')).legacyGuideContentStore.getOFDProject(id).state,fixture.templateId)
check('非法工程 JSON 被拒绝且原工程保留',JSON.stringify(after)===original)
await shot('30-invalid-import')
const content=await call('tran/v1/content/projects?type=GUIDE&keyword=REVIEW-GUIDE-20260910',session.accessToken)
const teaching=await call('tran/v1/teaching/guides?keyword=REVIEW-GUIDE-20260910',session.accessToken)
check('编制工程同步到正式内容库',content.status===200 && (content.data?.records||[]).some(r=>r.code==='REVIEW-GUIDE-20260910'),'实测仅写入浏览器本地库;正式内容库查询 HTTP '+content.status)
check('编制发布结果进入教学指导书列表',teaching.status===200 && (teaching.data?.records||[]).some(r=>r.code==='REVIEW-GUIDE-20260910'),'教学查询 HTTP '+teaching.status)
}
if (phase === 'viewport-probe') {
await open(`/content/ofd/${fixture.id}/studio?view=editor`)
const bytes=[...fs.readFileSync(path.resolve('../../文档/素材/09-Fox.glb'))]
console.log(await page.evaluate(async bytes=>{
const {createGuideViewport}=await import('/src/features/guide-legacy/demo-src/ofd-viewport.js')
const host=document.createElement('div'); host.style.cssText='position:fixed;inset:100px;background:white;z-index:99999';document.body.append(host)
const v=createGuideViewport(); window.reviewViewport=v;v.attach(host);await v.load('review-fox',{bytes:new Uint8Array(bytes)})
return {scene:v.describeScene(),fog:v.scene.fog,camera:v.camera.position.toArray(),target:v.controls.target.toArray(),stats:v.loadStats}
},bytes))
await page.waitForTimeout(1000); await shot('viewport-probe')
await page.evaluate(()=>{window.reviewViewport.scene.fog=null;window.reviewViewport.invalidate()})
await page.waitForTimeout(500);await shot('viewport-without-fog')
console.log(await page.evaluate(()=>{const v=window.reviewViewport;return {stats:v.renderer.info.render,disposed:v.disposed,needsRender:v.needsRender,camera:v.camera.position.toArray(),target:v.controls.target.toArray(),bounds:v.modelBox().getSize(new v.camera.position.constructor()).toArray()}}))
}
} catch(error) {
await shot(`${phase}-failure`).catch(()=>{})
check(`${phase} 脚本执行`, false, error.message)
process.exitCode=1
} finally {
if (!['offline','viewport-probe','reader'].includes(phase)) await context.storageState({ path:stateFile, indexedDB:true })
fs.writeFileSync(path.join(report, `${phase}.json`), JSON.stringify({ checks, errors, requests }, null, 2))
await browser.close()
}

+ 93
- 0
tools/guide-closure-report.mjs 查看文件

@@ -0,0 +1,93 @@
import fs from 'node:fs'
import path from 'node:path'

const root = path.resolve('reports/guide-closure-20260910')
const phases = ['create','edit','reader','missing','content','revision','templates','layout','api','offline','final-document','export','resources']
const runs = phases.map(phase => ({ phase, ...JSON.parse(fs.readFileSync(path.join(root, `${phase}.json`), 'utf8')) }))
const checks = runs.flatMap(run => run.checks.map(check => ({ phase:run.phase, ...check })))
const artifacts = JSON.parse(fs.readFileSync(path.join(root, 'artifact-validation.json'), 'utf8'))
const fixes = [
['不存在的工程地址自动新建', 'runtime.ts', '不存在 ID 明确报错,返回列表选择,避免查看操作创建脏数据。', 'missing'],
['列表默认排序及重置', 'GuideProjectList.vue / content-lists.js', '统一按创建时间倒序;更新旧工程不应改变默认顺序。', 'templates / layout'],
['只读用户无法选择导出和阅读控件', 'ofd-v4.js', '放行导出格式、页码、缩放、搜索等阅读控件;OFD 导出不再尝试保存草稿。', 'reader'],
['狐狸三维视口被环境雾完全遮住', 'ofd-viewport.js', '单模型加载清除车间雾;加载场景仍使用场景自身环境配置。', 'revision / offline'],
['默认模型相机距离不适配真实模型尺寸', 'ofd-document.js', '缺省相机走自动取景,保留用户明确保存的相机位置。已有显式相机不批量覆盖。', 'final-document / regression'],
['新建继承演示工程版本、反馈和装备参数', 'ofd-v4.js', '新工程清空演示业务记录、零件、技术参数与来源文件;模板章节按原模板生成。', 'templates'],
['开发环境导出的离线三维包加载失败', 'ofd-offline-runtime-assets.js', '打包 Three.js 和解码器原始源码,消除 Vite 改写后的开发路径。', 'offline / artifact-validation'],
['长标题、公式间距与动画秒数版式', 'ofd-layout.js', '缺省长标题自适应,公式后增加必要间距,动画时间保留两位小数。', 'PDF 四页目视 / regression'],
['反馈示例固定引用不存在的步骤', 'ofd-v4.js', '默认反馈示例使用当前工程的实际步骤,避免新建工程执行示例直接 404。', 'api:submit-feedback 201'],
]
// 通过只表示这里写明的观察范围,不代表正式业务链路验收。
const coverage = [
['列表','列表加载、编号搜索、重置、创建时间排序','本地通过','templates / layout','编辑旧记录后的多记录排序组合未单独量化。'],
['列表','项目详情、状态、版本入口','部分覆盖','代码审查 + publish 截图','统计是本地数据,不能作为教学统计。'],
['列表','删除及所有批量/权限组合','待补验证','代码审查','未删除用户工程;未对每一种授权组合做全矩阵测试。'],
['创建','空白向导、基本信息、建档','本地通过','create','正式后台不存在该新工程。'],
['创建','四类模板静态/动态预览','本地通过','templates','实际逐个打开并切换目录。'],
['创建','模板 PDF、OFD 下载和选模板创建','本地通过','templates','下载实测第一类;新建实测第四类(4 章 9 步)。'],
['创建','模板来源与平台模板管理联动','待确认改造','静态 OFD_CONTENT_TEMPLATES','当前不是后台模板库。'],
['创建','演示历史、反馈、参数隔离','修复通过','templates','仅修复新建,不清理用户既有文档。'],
['分析规划','资料来源、需求规格、构建计划导出','部分覆盖','02-planning / export','Markdown 下载已测;专业内容自动分析质量未验收。'],
['分析规划','多装备、多层级、关键步骤覆盖','待业务验收','原始 DOCX 对照','狐狸、卡车为功能夹具,不能证明真实维修覆盖率。'],
['编辑','步骤属性、保存、刷新重开','本地通过','edit','仅在同一测试浏览器项目空间持久化。'],
['编辑','新步骤、续页、复制、撤销重做','本地通过','edit','章节及步骤所有拖动组合未穷举。'],
['编辑','表格单元格、合并拆分、边框','本地通过','layout','填写后重开检查;未对所有样式排列组合做测试。'],
['编辑','自由文本块、矩形标注','本地通过','layout','自由排版允许用户自行移动重叠;未自动约束所有自由布局。'],
['编辑','公式输入、说明、关联区排版','修复通过','content / regression / PDF','两个公式回归与实际 PDF 检查。'],
['编辑','字体、纸张、页眉页脚、所有标注形状','部分覆盖','属性面板巡检与源码','不是每个属性都做了写入、重开、导出三重验证。'],
['资源','本地 GLB 导入、绑定步骤','本地通过','edit / revision','09-Fox.glb、08-CesiumMilkTruck.glb。'],
['资源','后台模型制作/场景制作资源选择','待确认改造','本地 platformModels / 场景目录','不能声称使用了之前在服务端创建的两个场景。'],
['资源','单模型显示、取景、截图、动画','修复通过','revision / final-document','实际图像和帧差验证,截图保存后重开保留。'],
['资源','本地视频导入、播放、截帧保存','本地通过','resources','使用 2 秒测试视频;不代表真实维修内容验证。'],
['资源','音频、资料包、损坏模型、存储配额','待补验证','导入代码审查','未对全部格式和存储异常做实际测试。'],
['资源','模型热点、部件聚焦、三维与步骤语义关联','部分覆盖','属性面板 + 离线观察操作','专业热点与真实装备语义尚需专项验证。'],
['预览','静态版式、动态步骤搜索与跳转','本地通过','layout / templates','在线预览位于本地编辑器。'],
['预览','播放、暂停/重播、观察控制','本地通过','revision / offline','两模型动画帧改变;离线观察按钮逐个执行。'],
['权限','管理员只读、导出不修改文档','修复通过','reader(实际 admin 会话)','编辑使用 root;未把三角色全接口授权矩阵当作已验证。'],
['权限','账号间工程隔离、数据范围','待确认改造','共享本地存储命名空间','正式数据应以现有后台权限和用户范围为准。'],
['发布','预检清单、草稿预览、发布面板','本地通过','08-preflight / 09-publish-panel','预检通过不等于后端发布成功。'],
['发布','发布、修订、旧快照、新版本、撤回','本地通过','content / revision','现状均为本地生命周期;没有实际发送后台发布请求。'],
['发布','后台版本、OFD/离线交付物原子发布','待确认改造','ContentProjectService','后台已要求 GUIDE 的 OFD + OFFLINE_HTML ZIP;现有页面未串联。'],
['发布','审核策略','代码确认','ContentProjectService GUIDE 分支','审核按现有配置可选;不引入强制审核新规则。'],
['导入导出','工程 JSON、导入恢复、非法 JSON','本地通过','revision / resources','JSON 不是完整资产备份;模型字节仍在 IndexedDB。'],
['导入导出','实际 PDF 下载及四页版式','修复通过','final-pdf-page-1 至 4','中文、目录、模型图、公式目视检查;字体存在替代。'],
['导入导出','实际 OFD ZIP/XML','本地通过','artifact-validation','未用第三方原生 OFD 阅读器验证标准一致性或签章。'],
['导入导出','离线 ZIP、GLB 轻量化、运行时','修复通过','offline / artifact-validation','卡车导出按已有逻辑压缩;结构与动画保留,非原字节复制。'],
['离线','断开公网/业务服务后本地启动阅读','本地通过','offline','Windows + Node.js 本地静态服务;没有依赖外站的请求。'],
['离线','手机尺寸、麒麟、真实移动设备','部分覆盖','23-offline-mobile','390×844 可显示,按钮换行与三维取景仍偏拥挤;未测实机、麒麟。'],
['教学','学员指导书列表、正式阅读内容','待确认改造','正式内容查询 + teaching 接口查询','不能从本地发布进入正式教学阅读。'],
['教学','收藏、个人笔记、检索、进度','待确认改造','原始需求与离线入口核对','离线缺个人笔记/收藏入口;需与在线用户身份及进度方案一并实现。'],
['教学','学习统计、报表、训练/考试联动','待确认改造','后端 teaching/guides 与原始需求','未因后台存在接口而判为全链路通过。'],
['接口','九个本地契约操作与反馈样例','修复通过','api / revision','八个本地请求逐个执行;生命周期通过发布 UI 单独实测。'],
['接口','第三方 HTTP 调用、鉴权、正式反馈','待确认改造','api:零真实业务 HTTP 请求','显示 /api/ofd 不代表服务端真实提供该接口。'],
['质量','构建、单元回归、浏览器异常','通过','build 及 20 项测试','选定最终 E2E 阶段无 pageerror;不包含早期定位脚本失败。'],
['质量','三主题、键盘、长文档、并发和性能指标','待补验证','本轮边界','未验证所有组合;不能宣称 <1s / ≥25fps / ≥99% / ≤10s 已达标。'],
]
const proposals = [
['A:编辑与正式内容闭环(优先)','保留现有编辑器,将列表、工程保存、模板/模型/场景选择、权限和版本操作对接现有 content 接口。服务端为数据真值;明确迁移本地工程和 IndexedDB 文件,不能静默覆盖。发布时提交 OFD 和离线 ZIP,并遵循现有可选审核设置。','验收:新建→编辑→刷新/换浏览器→发布→旧版本不变→撤回;正式数据库与界面一致,其他账号按权限可见。'],
['B:教学阅读闭环','衔接现有教学指导书、阅读记录和反馈接口;补齐个人笔记、收藏、学习进度与统计。离线笔记是否回传及冲突处理需要与在线范围一起定。','验收:教员发布→学员阅读→记录/笔记/反馈保存→教员统计可查询;账号隔离生效。'],
['C:对外接口真实联调','将当前本地契约自检与真实 HTTP 接口对齐,文档标明真实路径、参数、错误及授权;沿用现有平台认证机制,不新增密钥文件或独立服务。','验收:从独立客户端调用,读取/修改/反馈真实工程,未授权请求被拒绝。'],
]
const pictures = [
['19-list-created-order.png','新建模板工程与创建时间排序'],['01-create-wizard.png','空白创建向导'],['02-planning.png','资料与规划'],['17-template-preview.png','模板结构预览'],
['27-final-fox.png','狐狸真实模型显示与取景'],['28-final-truck.png','卡车真实模型显示与取景'],['29-video-poster.png','视频播放与当前帧海报'],['30-invalid-import.png','非法工程导入保留原文档'],
['24-table-edit.png','表格编辑测试(自由块为测试内容)'],['25-markup.png','矩形标注'],['26-interactive-search.png','动态阅读搜索'],['16-withdrawn.png','本地版本撤回'],
['20-api-local.png','接口自检:实际仍为本地调度'],['21-offline-reading.png','离线步骤阅读'],['22-offline-model-0.png','离线狐狸三维'],['22-offline-model-1.png','离线卡车三维'],['23-offline-mobile.png','手机尺寸观察:仍有拥挤'],
...[1,2,3,4].map(n=>[`final-pdf-page-${n}.png`,`最终 PDF 第 ${n} 页`]),
]
const summary = {date:'2026-09-10',conclusion:'未完全闭环;本地编辑与导出路径完成修复验证,正式内容、教学和对外 HTTP 接入待确认',environment:{web:'http://127.0.0.1:6180',gateway:6100,browser:'Edge headless / Playwright',viewport:'1920×1080;390×844 观察',accounts:'root 编辑、admin 只读(不含凭据)'},pass:checks.filter(c=>c.status==='PASS').length,fail:checks.filter(c=>c.status==='FAIL').length,unitTests:20,artifactChecks:artifacts.checks,fixes,coverage,proposals,runs}
fs.writeFileSync(path.join(root,'summary.json'),JSON.stringify(summary,null,2))
const esc = s => String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))
const table = (heads,rows) => `<div class="table"><table><thead><tr>${heads.map(x=>`<th>${esc(x)}</th>`).join('')}</tr></thead><tbody>${rows.map(row=>`<tr>${row.map(x=>`<td>${esc(x)}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`
const html=`<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>作业指导闭环检查 · 20260910</title><style>
body{margin:0;background:#eef4f3;color:#173d36;font:15px/1.7 system-ui,"Microsoft YaHei",sans-serif}main{max-width:1300px;margin:auto;padding:30px}header,section{background:white;border:1px solid #c9ded8;border-radius:12px;padding:26px;margin-bottom:20px}h1{margin:0 0 12px;font-size:28px}h2{margin:0 0 15px;font-size:22px}h3{font-size:18px}.notice{padding:18px;border-left:5px solid #cf8618;background:#fff5e5}.stats{display:flex;gap:15px;flex-wrap:wrap}.stats b{padding:12px 20px;border-radius:8px;background:#e9f5ef}.table{overflow:auto}table{border-collapse:collapse;width:100%;font-size:14px}td,th{border:1px solid #cfdfdb;padding:10px;vertical-align:top;text-align:left}th{background:#edf6f2}a{color:#087e62}.images{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}figure{margin:0;border:1px solid #d8e4e0;padding:10px;border-radius:8px}figure img{width:100%;height:290px;object-fit:contain;background:#f1f5f4}figcaption{font-size:14px}code{background:#edf4f2;padding:2px 5px}nav{display:flex;gap:20px;flex-wrap:wrap}small{color:#56726b}@media(max-width:700px){main{padding:12px}section,header{padding:16px}.images{grid-template-columns:1fr}}@media print{body{background:white}main{padding:0}section{break-inside:auto}.images{grid-template-columns:1fr 1fr}a{color:inherit}}</style>
<main><header><h1>作业指导模块:闭环检查与修复报告</h1><p>2026-09-10 · 资料 → 原型 → 设计 → 开发 → 测试</p><div class="notice"><strong>结论:未完全闭环。</strong>本地编制、三维/视频预览与导出已完成本轮验证;实际页面仍保存到浏览器本地库,尚未连通正式内容发布、教学阅读及对外 HTTP 接口。不能以本地“已发布”认定全系统发布成功。</div><p class="stats"><b>${summary.pass} 项 E2E 断言通过</b><b>${summary.fail} 项需求断言未满足</b><b>20 项回归通过</b><b>6 项交付物检查通过</b><b>构建通过</b></p><p>未满足的 5 项中,4 项重复印证正式内容/教学链路缺失,1 项为离线个人笔记和收藏入口缺失。通过项含“确认现状”的断言,不能直接换算为产品完成率。</p><nav><a href="#source">资料与范围</a><a href="#fixes">已修复</a><a href="#coverage">功能覆盖</a><a href="#confirm">待确认方案</a><a href="#evidence">截图</a><a href="#tests">测试明细</a></nav></header>
<section id="source"><h2>1. 资料、原型与设计核对</h2><p>原始依据:<code>文档/桥梁装备“数字车间”资源建设需求参数.docx</code>。覆盖内容分析规划、标准模板、排版与媒体关联、PDF/离线交付、版本更新、教学阅读、个人记录、反馈和统计。未将未阅读的其他 PDF 算作本轮依据。</p><p>正式路由经 <code>LegacyGuidePageView.vue → runtime.ts → ofd-v4.js</code> 托管原型。工程存储为带命名空间的本地 ContentListStore(seed:false),资源字节在 IndexedDB;模板和平台资源选择仍使用本地实现。后台已有 content projects、guide templates、teaching guides 接口,现状是前后两条实现未衔接。</p><p>测试创建了空白狐狸文档和模板卡车文档;狐狸文档中加入卡车作为第二个模型步骤,对两种模型验证导出与离线阅读。由于服务端场景资源没有接通,本轮直接导入本地 GLB,不能声称走通了之前正式创建的狐狸/卡车场景引用。</p><p>小修保持原交互和文档结构;不重建编辑器、不新增独立服务、不改 DDL。本轮不提交 Git。</p></section>
<section id="fixes"><h2>2. 开发修复</h2>${table(['问题','文件','处理','验证'],fixes)}</section>
<section id="coverage"><h2>3. 全模块功能覆盖与边界</h2><p>按功能入口建立 ${coverage.length} 项检查矩阵。“部分覆盖”和“待补验证”明确保留,不表示已跑过所有按钮与组合。</p>${table(['范围','功能点','结论','证据','边界'],coverage)}</section>
<section id="confirm"><h2>4. 需要确认的较大调整</h2><p>建议按 A → B → C 实施,复用现有后端和编辑器。以下尚未实施,避免在本次 bugfix 中静默改变存储、权限和发布规则。</p>${table(['范围','具体实施','验收闭环'],proposals)}<p>另有后续验证项:移动窄屏按钮与取景拥挤、完整主题/键盘矩阵、原生 OFD 阅读器、麒麟、长文档与性能压力测试。真实装备内容覆盖率及专业维修正确性需业务资料与实际装备验收。</p></section>
<section><h2>5. 交付文件与复核方法</h2><p><a href="export-pdf.pdf">下载实测 PDF</a> · <a href="export-ofd.ofd">下载 OFD</a> · <a href="export-offline.zip">下载离线 ZIP</a> · <a href="export-project.json">下载工程 JSON</a> · <a href="export-requirement.md">需求规格</a> · <a href="export-plan.md">构建计划</a> · <a href="summary.json">完整 JSON 报告</a></p><p>离线 ZIP 完整解压后用其 Windows 启动脚本运行,需要已有 Node.js,通过本机 HTTP 打开,不能直接 file://。运行时和两个 GLB 均在包内,无公网或业务 API 依赖。OFD 已检查 ZIP/XML,PDF 四页已渲染核对;未宣称第三方 OFD 认证,PDF 字体跨平台一致性未验证。</p><p>测试数据保留在隔离的自动化浏览器空间,不在用户日常浏览器或服务端列表中。工程 JSON 含资源引用,不含所有模型原字节,导入编辑时需重新选择本地素材;离线 ZIP 是可独立复核的阅读交付物。下载内容使用测试文字,不能用于实际维修操作。</p>${table(['交付物检查','结果','说明'],artifacts.checks.map(c=>[c.name,c.status,c.detail]))}</section>
<section id="evidence"><h2>6. 实际截图</h2><p>点击查看原图。截图用于证明对应操作,含测试自由文本/标注的页面不作为最终排版模板。</p><div class="images">${pictures.map(([file,title])=>`<figure><a href="${esc(file)}"><img loading="lazy" src="${esc(file)}" alt="${esc(title)}"></a><figcaption>${esc(title)}</figcaption></figure>`).join('')}</div></section>
<section id="tests"><h2>7. 测试明细</h2><p>环境:Windows、Edge headless、Playwright,1920×1080;前端 6180,经 Gateway 6100;root 编辑、admin 只读。离线包通过隔离的 18766 本地静态服务运行并阻断其他主机请求。未将浏览器登录凭据写入报告。</p><p>命令:<code>node tools/guide-closure-live.mjs &lt;phase&gt;</code>(多阶段顺序执行,不能并发共享浏览器状态);<code>python tools/guide-artifact-check.py</code>;<code>node --test scripts/guide-legacy-closure.test.mjs</code>;现有 5 个 guide 测试脚本(17 项);<code>pnpm run build</code>(含 vue-tsc 和构建后校验)。新旧单元测试共 20 项通过,构建完成,仅既有 chunk size 提示。</p><p>早期脚本定位失败、重复运行造成的选择器歧义、Playwright storageState 无法恢复 Blob 原型,都未作为产品缺陷统计;实际模型测试通过界面重新导入,再在同一上下文刷新验证持久化。原始中间记录保留用于追溯,最终统计只采用下面选定阶段。</p>${table(['阶段','断言','结果','说明'],checks.map(c=>[c.phase,c.name,c.status,c.detail]))}<p>最终选定阶段 pageerror 总数:${runs.reduce((n,r)=>n+r.errors.length,0)}。真实 HTTP 业务写请求为零是尚未接入的证据,而不是接口性能验证。</p></section></main></html>`
fs.writeFileSync(path.join(root,'index.html'),html)
console.log(JSON.stringify({pass:summary.pass,fail:summary.fail,coverage:coverage.length,screenshots:pictures.length}))

Loading…
取消
儲存