ソースを参照

test: 补充数字人开放接口与编排交互验证报告

main
leiyun 1週間前
コミット
8d421bd7b4
74個のファイルの変更5943行の追加0行の削除
  1. バイナリ
     
  2. バイナリ
     
  3. バイナリ
     
  4. +8
    -0
      reports/advanced-drag-layout-20260909/index.html
  5. +40
    -0
      reports/advanced-drag-layout-20260909/results.json
  6. バイナリ
     
  7. バイナリ
     
  8. バイナリ
     
  9. バイナリ
     
  10. バイナリ
     
  11. バイナリ
     
  12. バイナリ
     
  13. バイナリ
     
  14. バイナリ
     
  15. バイナリ
     
  16. バイナリ
     
  17. +35
    -0
      reports/digital-human-open-api-20260908/cleanup-results.json
  18. バイナリ
     
  19. バイナリ
     
  20. +43
    -0
      reports/digital-human-open-api-20260908/index.html
  21. +14
    -0
      reports/digital-human-open-api-20260908/java-test-results.json
  22. +26
    -0
      reports/digital-human-open-api-20260908/playback-results.json
  23. バイナリ
     
  24. +59
    -0
      reports/digital-human-open-api-20260908/security-results.json
  25. バイナリ
     
  26. +22
    -0
      reports/digital-human-open-api-20260908/workshop-boundaries.json
  27. +27
    -0
      reports/digital-human-open-api-20260908/workshop-results.json
  28. バイナリ
     
  29. バイナリ
     
  30. バイナリ
     
  31. バイナリ
     
  32. バイナリ
     
  33. バイナリ
     
  34. +12
    -0
      reports/narration-dialog-layout-20260909/index.html
  35. +73
    -0
      reports/narration-dialog-layout-20260909/results.json
  36. バイナリ
     
  37. バイナリ
     
  38. バイナリ
     
  39. +18
    -0
      reports/open-platform-pdf-20260909/index.html
  40. +9
    -0
      reports/open-platform-pdf-20260909/results.json
  41. バイナリ
     
  42. バイナリ
     
  43. バイナリ
     
  44. バイナリ
     
  45. バイナリ
     
  46. バイナリ
     
  47. バイナリ
     
  48. バイナリ
     
  49. バイナリ
     
  50. バイナリ
     
  51. バイナリ
     
  52. +28
    -0
      reports/open-platform-style-20260909/index.html
  53. バイナリ
     
  54. +16
    -0
      reports/open-platform-style-20260909/results.json
  55. バイナリ
     
  56. バイナリ
     
  57. バイナリ
     
  58. バイナリ
     
  59. バイナリ
     
  60. バイナリ
     
  61. バイナリ
     
  62. +3126
    -0
      reports/training-step-layout-20260908/after.json
  63. バイナリ
     
  64. バイナリ
     
  65. バイナリ
     
  66. バイナリ
     
  67. バイナリ
     
  68. バイナリ
     
  69. +2083
    -0
      reports/training-step-layout-20260908/before.json
  70. +12
    -0
      reports/training-step-layout-20260908/index.html
  71. +75
    -0
      tools/advanced-drag-layout.mjs
  72. +79
    -0
      tools/narration-dialog-layout.mjs
  73. +40
    -0
      tools/open-platform-pdf.mjs
  74. +98
    -0
      tools/open-platform-style.mjs

バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


+ 8
- 0
reports/advanced-drag-layout-20260909/index.html ファイルの表示

@@ -0,0 +1,8 @@
<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>高级编排拖动边界修复验证</title><style>body{margin:0;background:#f2f6f4;color:#173c32;font:16px/1.7 system-ui,sans-serif}main{max-width:1100px;margin:30px auto;padding:28px;background:white;border-radius:12px}h1{margin-top:0}h2{font-size:20px;margin-top:28px}img{max-width:100%;border:1px solid #cdded5}a{color:#087d5b}table{width:100%;border-collapse:collapse}td,th{border:1px solid #cdded5;padding:9px;text-align:left}th{background:#edf4f0}</style><main>
<h1>高级编排拖动边界修复验证</h1><p>2026-09-09 · 开发环境 127.0.0.1:6180 · 验证通过</p>
<p>原因:节点拖动使用画布可见区域的宽高限制坐标,未使用完整滚动区域,也未考虑拖动时的滚动偏移。下方仍有网格时,节点会提前被拦住。</p>
<p>修复:边界按完整画布及节点实际尺寸计算,鼠标坐标转换为滚动后的画布坐标。重新选中节点等界面重绘保留同一工程的滚动位置;切换工程时从起点显示。</p>
<table><tr><th>检查项</th><th>结果</th></tr><tr><td>拖到下方网格</td><td>通过:原限制 y=365,实际拖到 y=535.75,节点与连线正常显示。</td></tr><tr><td>拖动中滚动画布</td><td>通过:滚动 180px、鼠标移动 5px,节点准确移动 185px。</td></tr><tr><td>重新选中节点</td><td>通过:节点坐标及画布滚动位置保持。</td></tr><tr><td>窄窗口横向滚动后拖动</td><td>通过:节点从 (769,307) 移至 (779,317),与鼠标 10px 位移一致。</td></tr><tr><td>基础/高级编排切换</td><td>通过:返回高级编排后保留调整后的节点坐标。</td></tr><tr><td>构建及检查</td><td>pnpm run build 通过,包含类型检查与模型编辑器生成包校验;git diff --check 通过。</td></tr></table>
<p><a href="results.json">自动化测试结果</a></p><h2>下方网格拖动结果</h2><img src="01-lower-grid.png" alt="开始节点已拖至下方空白网格区域">
<h2>横向滚动后小幅拖动</h2><img src="02-horizontal-scroll.png" alt="窄窗口横向滚动后节点按鼠标位移移动">
<h2>测试范围与数据</h2><p>使用账号文件中的内部维护账号,在已有测试工程 410 上操作,仅修改临时浏览器草稿,未点击保存或发布。没有新增或删除业务记录;本轮不包含服务端保存后重载验收。</p><p>复现脚本:ute2e/tools/advanced-drag-layout.mjs。未修改接口、数据库或训练执行逻辑。</p></main></html>

+ 40
- 0
reports/advanced-drag-layout-20260909/results.json ファイルの表示

@@ -0,0 +1,40 @@
{
"passed": true,
"projectId": "410",
"accountType": "内部维护",
"initial": {
"x": 27,
"y": 48
},
"checks": [
{
"name": "拖入下方网格,拖动中滚动不跳位",
"result": "PASS",
"before": 350.75,
"after": 535.75,
"scrollDelta": 180,
"formerLimit": 365
},
{
"name": "选择节点后保留画布滚动位置",
"result": "PASS"
},
{
"name": "窄窗口横向滚动后拖动不跳位",
"result": "PASS",
"previous": {
"x": 769,
"y": 307
},
"next": {
"x": 779,
"y": 317
}
},
{
"name": "切换编排模式保留节点坐标",
"result": "PASS"
}
],
"dataChanges": "仅调整已有测试工程的浏览器草稿,未点击保存或发布。"
}

バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


+ 35
- 0
reports/digital-human-open-api-20260908/cleanup-results.json ファイルの表示

@@ -0,0 +1,35 @@
{
"cleaned": [
{
"type": "temporary-platform-video",
"id": 51,
"deleted": true,
"filesRetained": true,
"jobHistoryRetained": true
},
{
"type": "temporary-platform-video",
"id": 52,
"deleted": true,
"filesRetained": true,
"jobHistoryRetained": true
},
{
"type": "isolated-test-application",
"appId": "AP_617ADA153EFCA5EBC66DA5AF",
"enabled": false
}
],
"retained": {
"configuredApplication": "AP_238179E0A10F97927EA49FA3",
"workshopProjectIds": [
"410",
"411"
],
"draftAssignmentIds": [
"52",
"53"
],
"reason": "Teaching API has no draft delete endpoint; fixed-version training copies and controlled imported video copies remain for review. No learner runs were created. Platform video deletion retains files and job audit by existing contract."
}
}

バイナリ
ファイルの表示


バイナリ
ファイルの表示


+ 43
- 0
reports/digital-human-open-api-20260908/index.html ファイルの表示

@@ -0,0 +1,43 @@
<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>数字人开放 API 与数字车间讲解联调报告</title><style>
:root{color-scheme:light;font:15px/1.75 "Microsoft YaHei",system-ui,sans-serif;color:#183c40;background:#edf4f3}*{box-sizing:border-box}body{margin:0}main{max-width:1180px;margin:auto;padding:34px 24px 70px}h1{font-size:29px;line-height:1.4}h2{font-size:21px;margin:0 0 16px}h3{font-size:17px}section,header{background:#fff;border:1px solid #d1e3df;border-radius:12px;padding:24px;margin:18px 0}header{border-top:5px solid #138771}p{margin:10px 0}.muted,figcaption{color:#54716e;font-size:14px}.pass{color:#08764f;font-weight:700}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.stats div{background:#edf8f4;border-radius:8px;padding:16px}.stats strong{font-size:25px;display:block;color:#08764f}table{width:100%;border-collapse:collapse;margin:15px 0}th,td{text-align:left;border-bottom:1px solid #dce8e5;padding:11px;vertical-align:top}th{background:#f2f7f6}code{background:#eff5f4;padding:2px 5px;border-radius:4px;overflow-wrap:anywhere}a{color:#087862}figure{margin:22px 0}img{display:block;width:100%;height:auto;border:1px solid #cadeda;border-radius:8px}figcaption{padding:9px 2px}.videos{display:grid;grid-template-columns:1fr 1fr;gap:18px}video{width:100%;background:#142d3c;max-height:350px}li{margin:7px 0}summary{cursor:pointer;font-weight:600}pre{white-space:pre-wrap;overflow-wrap:anywhere;background:#f5f8f7;padding:15px;font-size:13px}.note{border-left:4px solid #b17f24;padding-left:15px}@media(max-width:760px){main{padding:12px}section,header{padding:16px}.stats,.videos{grid-template-columns:1fr 1fr}table{font-size:13px}.videos{grid-template-columns:1fr}}
td.pass{min-width:72px;white-space:nowrap}th{white-space:nowrap}
</style></head><body><main>
<header><p class="muted">开发环境实测 · 2026-09-08—09 · 资料 → 原型 → 设计 → 开发 → 测试</p><h1>数字人开放 API 与数字车间讲解联调</h1><p class="pass">三阶段实施完成:独立应用授权、真实资源库、服务端生成成片并绑定训练步骤。</p><p>联调 APPID:<code>AP_238179E0A10F97927EA49FA3</code>。密钥已配置在数字车间服务端加密存储,报告不包含密钥、令牌或账号口令。</p><div class="stats"><div><strong>3 / 3</strong>实施阶段完成</div><div><strong>533</strong>后端自动化检查通过</div><div><strong>18</strong>真实接口边界检查</div><div><strong>2</strong>发布版本视频播放验证</div></div></header>

<section><h2>实施结果</h2><table><thead><tr><th>阶段</th><th>交付内容</th><th>结果</th></tr></thead><tbody>
<tr><td>1. 数字人开放平台</td><td>开放应用管理;AP_ APPID;一次性密钥;重置/停用/令牌撤销;接口 Scope、形象/音色/背景/目录/视频授权;限频与并发;公开 API 文档和 OpenAPI 下载。</td><td class="pass">通过</td></tr>
<tr><td>2. 数字车间接入</td><td>数字人类型独立配置;Auth 加密保管 Secret;Tran 使用受保护的内部短期令牌接口;真实资源分页、视频预览、导入、步骤绑定、保存和刷新。</td><td class="pass">通过</td></tr>
<tr><td>3. 现场生成成片</td><td>服务端 TTS + 配对 Viewer + FFmpeg;数据库任务/租约/幂等;关闭刷新恢复、取消重试;生成背景与字幕;成片保存后导入;发布版本受控播放。</td><td class="pass">通过</td></tr>
</tbody></table><p>公开文档:<a href="http://127.0.0.1:8003/developer" target="_blank">数字人开放 API 说明</a>。应用管理:数字人系统 → 系统设置 → 开放应用。数字车间:系统管理 → 接口对接 → 数字人。</p></section>

<section><h2>真实业务链路</h2><table><thead><tr><th>测试副本</th><th>过程</th><th>最终证据</th></tr></thead><tbody>
<tr><td>狐狸 · 工程 410</td><td>资源库选择真实成片 → 播放 → 导入为 VIDEO → 绑定第一步 → 保存 → 刷新验证 → 发布 → 对抗训练预览播放。</td><td>发布版本 466,预览任务 52;640 × 360,4.52 秒。</td></tr>
<tr><td>牛奶卡车 · 工程 411</td><td>选择形象与音色、数字化教室背景 → 输入文稿 → 提交生成 → 关闭/刷新 → 恢复同一任务 → 成片预览 → 导入绑定 → 发布 → 实装实训预览播放。</td><td>发布版本 467,预览任务 53;1280 × 720,4.60 秒。</td></tr>
</tbody></table><p>以下视频为本次接口下载的真实生成文件,可直接播放。数字车间保存了独立的受控文件副本;数字人平台临时视频清理后,固定发布版本仍能播放。</p><div class="videos"><div><h3>狐狸:资源库视频</h3><video controls preload="metadata" src="fox-narration.mp4"></video><a href="fox-narration.mp4" download>下载 MP4(190,333 字节)</a></div><div><h3>牛奶卡车:现场生成成片</h3><video controls preload="metadata" src="truck-narration.mp4"></video><a href="truck-narration.mp4" download>下载 MP4(430,221 字节)</a></div></div></section>

<section><h2>过程发现与修复</h2><table><thead><tr><th>问题</th><th>修复与验证</th></tr></thead><tbody>
<tr><td>并发资源查询占满小连接池</td><td>调用审计移到完整请求释放数据库连接后执行;三个目录并发真实请求 516 ms 完成,均为 200,补充顺序回归。</td></tr>
<tr><td>本地缓存缺失时无法读取远端文件</td><td>复用统一文件服务的受控取回及完整性校验;不再仅判断本地路径。真正缺失的背景文件给出明确失败原因。</td></tr>
<tr><td>教学资源不识别 fsvc://</td><td>在原固定版本、数据权限及考试白名单基础上支持统一文件服务地址,验证发布后实际播放及跨版本拒绝访问。</td></tr>
<tr><td>生成提交后关闭/刷新导致上下文丢失</td><td>保存工程、步骤、APPID、请求键与远程任务映射;重开恢复,提交结果不确定时用原幂等键确认,应用切换不能串用任务。</td></tr>
<tr><td>旧视频绑定与已删除步骤</td><td>切换来源清除旧活动绑定;生成前确认步骤存在与版本一致;数字人来源没有 READY VIDEO 时不能发布。</td></tr>
<tr><td>上传凭证篡改检查存在偶发失败</td><td>拒绝带非规范补位的 Base64 表示,保持原 HMAC、人员/工程/版本和文件摘要校验;已有失败用例回归通过。</td></tr>
</tbody></table><p class="note">两条历史背景记录(85、79)的源文件在当前环境无法取回,未修改原资料。本次为联调应用授权实际可用的“数字化教室”背景(11)。缺失素材需补传后使用,系统不会伪造成功结果。</p></section>

<section><h2>截图证据</h2><p class="muted">截图来自真实 Edge 浏览器。点击可查看原图;中间过程图展示拍摄时状态,最终结果以下方发布播放截图及 JSON 为准。</p>
<figure><a href="01-open-applications.png" target="_blank"><img src="01-open-applications.png" loading="lazy" alt="数字人开放应用列表"></a><figcaption>开放应用列表:按应用管理 APPID、状态、授权和限额。</figcaption></figure>
<figure><a href="02-application-grants.png" target="_blank"><img src="02-application-grants.png" loading="lazy" alt="配置开放应用授权"></a><figcaption>配置页:独立接口权限及资源授权,未展示密钥。</figcaption></figure>
<figure><a href="03-open-api-docs.png" target="_blank"><img src="03-open-api-docs.png" loading="lazy" alt="公开开放API文档"></a><figcaption>公开文档:认证、生成/重试、响应和动态 OpenAPI 契约。</figcaption></figure>
<figure><a href="04-workshop-library.png" target="_blank"><img src="04-workshop-library.png" loading="lazy" alt="数字车间真实数字人视频资源库"></a><figcaption>数字车间查询真实授权视频,不使用原型固定目录和占位视频。</figcaption></figure>
<figure><a href="06-persisted-binding.png" target="_blank"><img src="06-persisted-binding.png" loading="lazy" alt="狐狸工程刷新后保留讲解视频绑定"></a><figcaption>狐狸工程刷新后读取服务端保存的绑定。</figcaption></figure>
<figure><a href="07-generation-settings.png" target="_blank"><img src="07-generation-settings.png" loading="lazy" alt="牛奶卡车讲解生成参数"></a><figcaption>牛奶卡车成片参数:真实形象、音色、背景及讲解文稿。</figcaption></figure>
<figure><a href="08-job-restored.png" target="_blank"><img src="08-job-restored.png" loading="lazy" alt="刷新恢复生成任务"></a><figcaption>关闭并刷新后恢复已提交任务,不依赖原网页继续运行。</figcaption></figure>
<figure><a href="10-playback-fox.png" target="_blank"><img src="10-playback-fox.png" loading="lazy" alt="对抗训练预览中播放狐狸步骤讲解"></a><figcaption>狐狸发布版本:在对抗训练预览实际播放 1 秒后截图,可见数字人画面与字幕。</figcaption></figure>
<figure><a href="10-playback-truck.png" target="_blank"><img src="10-playback-truck.png" loading="lazy" alt="实装实训预览中播放牛奶卡车步骤讲解"></a><figcaption>牛奶卡车发布版本:在实装实训预览实际播放,包含背景及对应讲解字幕。</figcaption></figure>
</section>

<section><h2>验证范围与证据文件</h2><ul><li>Python:31 项通过;Auth:123 项通过;Tran:379 项通过。两个前端通过类型检查与生产构建,Java 包构建通过。</li><li>真实接口:未认证拒绝、应用/用户令牌隔离、跨应用生成视频拒绝、Range、公开契约、密钥轮换与旧令牌撤销、并发资源查询、删除步骤/旧版本/只读管理员拒绝、内部服务密钥保护及搜索边界。</li><li>任务幂等、并发、取消/重试、跨应用任务隔离和租约失效由自动化回归覆盖;真实浏览器覆盖实际生成、恢复、导入及发布后播放。</li><li>没有把本次视频播放测试扩写为完整实装识别或对抗计分验收。纯本地对抗演示尚无正式发布版本引用,本次不重构该业务域;正式任务宿主已补充讲解媒体入口。</li></ul><p><a href="java-test-results.json">Java 测试汇总</a> · <a href="security-results.json">真实开放接口检查</a> · <a href="workshop-boundaries.json">数字车间权限与版本边界</a> · <a href="workshop-results.json">编辑器导入与生成</a> · <a href="playback-results.json">发布播放验证</a> · <a href="cleanup-results.json">清理与残留记录</a></p></section>

<section><h2>部署、清理与保留数据</h2><p>开发库已执行数字人 033、数字车间 037/038,增量 DDL、完整 SQL、设计和接口文档同步。成片服务使用配置的共享数字人 Viewer、TTS、FFmpeg 与受控文件服务;运行浏览器及加密密钥文件需要随部署配置,不能只复制前端。</p><p>已清理数字人平台临时视频 51/52,并停用隔离测试应用;按平台原有删除契约,文件及任务审计历史保留。联调应用继续启用,APPID/密钥已配置,供继续使用。</p><p>数字车间保留测试副本 410/411、其独立导入视频及草稿教学任务 52/53。现有教学 API 没有草稿删除入口,保留清单供检查;未创建学员运行、成绩,未修改原有狐狸/卡车模型、场景或训练工程。两个草稿任务尚未面向学员发布。</p><p class="muted">本报告仅对应开发环境。修改尚未提交或推送 Git。</p></section>
</main></body></html>

+ 14
- 0
reports/digital-human-open-api-20260908/java-test-results.json ファイルの表示

@@ -0,0 +1,14 @@
{
"ut_auth": {
"tests": 123,
"failures": 0,
"errors": 0,
"skipped": 0
},
"ut_tran": {
"tests": 379,
"failures": 0,
"errors": 0,
"skipped": 0
}
}

+ 26
- 0
reports/digital-human-open-api-20260908/playback-results.json ファイルの表示

@@ -0,0 +1,26 @@
[
{
"projectId": "410",
"publishedVersionId": "466",
"assignmentId": "52",
"channel": "CONFRONTATION",
"assetCode": "DH_B66C365A317E448BBD7D513B5DC65FFF",
"media": {
"duration": 4.52,
"width": 640,
"height": 360
}
},
{
"projectId": "411",
"publishedVersionId": "467",
"assignmentId": "53",
"channel": "PHYSICAL",
"assetCode": "DH_C20425C1EDE14E7DB88BAD697E3D5AFD",
"media": {
"duration": 4.6,
"width": 1280,
"height": 720
}
}
]

バイナリ
ファイルの表示


+ 59
- 0
reports/digital-human-open-api-20260908/security-results.json ファイルの表示

@@ -0,0 +1,59 @@
{
"checks": [
{
"name": "Unauthenticated catalog denied",
"status": 401
},
{
"name": "Other application cannot download generated video",
"status": 404
},
{
"name": "Application token cannot access management",
"status": 401
},
{
"name": "User token cannot access application API",
"status": 401
},
{
"name": "Controlled Range download",
"status": 206
},
{
"name": "Public OpenAPI schema",
"status": 200
},
{
"name": "Old token revoked after secret rotation",
"status": 401
},
{
"name": "Old secret rejected",
"status": 401
},
{
"name": "Rotated credentials accepted",
"status": 200
},
{
"name": "Parallel resource avatars",
"status": 200
},
{
"name": "Parallel resource voices",
"status": 200
},
{
"name": "Parallel resource backgrounds",
"status": 200
},
{
"name": "Concurrent resource requests complete",
"milliseconds": 516
}
],
"appId": "AP_238179E0A10F97927EA49FA3",
"isolatedAppId": "AP_617ADA153EFCA5EBC66DA5AF",
"time": "2026-09-08T17:14:33.315Z"
}

バイナリ
ファイルの表示


+ 22
- 0
reports/digital-human-open-api-20260908/workshop-boundaries.json ファイルの表示

@@ -0,0 +1,22 @@
[
{
"name": "Deleted step cannot create generation",
"status": 400
},
{
"name": "Stale project version rejected",
"status": 409
},
{
"name": "Administrator viewing permission does not grant editing",
"status": 403
},
{
"name": "Internal runtime endpoint requires service key",
"status": 401
},
{
"name": "Search keyword does not alter fixed endpoint path",
"status": 200
}
]

+ 27
- 0
reports/digital-human-open-api-20260908/workshop-results.json ファイルの表示

@@ -0,0 +1,27 @@
{
"fixtures": [
{
"key": "fox",
"id": "410",
"name": "APE2E-数字人讲解-狐狸-20260909"
},
{
"key": "truck",
"id": "411",
"name": "APE2E-数字人讲解-牛奶-20260909"
}
],
"foxVideoAssetCode": "DH_B66C365A317E448BBD7D513B5DC65FFF",
"truckVideoAssetCode": "DH_C20425C1EDE14E7DB88BAD697E3D5AFD",
"tests": [
"real catalog",
"video preview",
"import and persist",
"reload binding",
"server generation",
"close and reload task",
"background and captions",
"generated import and persist"
],
"time": "2026-09-08T17:08:55.642Z"
}

バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


+ 12
- 0
reports/narration-dialog-layout-20260909/index.html ファイルの表示

@@ -0,0 +1,12 @@
<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>数字人讲解弹窗布局验证</title><style>body{margin:0;background:#f2f6f4;color:#173c32;font:16px/1.7 system-ui,sans-serif}main{max-width:1100px;margin:30px auto;padding:28px;background:white;border-radius:12px}h1{margin-top:0}h2{font-size:20px;margin-top:28px}img{max-width:100%;border:1px solid #cdded5}a{color:#087d5b}table{width:100%;border-collapse:collapse}td,th{border:1px solid #cdded5;padding:9px;text-align:left}th{background:#edf4f0}.note{background:#fff5e5;padding:14px}.mobile{max-width:390px}</style><main>
<h1>数字人讲解弹窗布局验证</h1><p>2026-09-09 · 布局修复通过 · 开发环境 6180 → 6100 Gateway</p>
<p>原弹窗叠加固定高度列表和视频后超过窗口。现限制弹窗最大高度,标题及底部关闭按钮固定,正文独立滚动;列表改为最大高度,少量记录不再保留大块空白。窄屏筛选和生成表单改为单列,预览视频保持比例并适应窗口。</p>
<table><tr><th>检查</th><th>结果</th></tr><tr><td>1920 × 911 桌面</td><td>通过,列表、视频与底部关闭完整显示。</td></tr><tr><td>1366 × 600 矮窗口</td><td>通过,正文滚动,关闭按钮保留在窗口内。</td></tr><tr><td>390 × 844 手机宽度</td><td>通过,筛选单列,表格可横向滚动,弹窗不越界。</td></tr><tr><td>1366 × 768 暗色主题</td><td>通过,使用系统主题切换检查。</td></tr><tr><td>生成表单、生成任务与关闭</td><td>通过,可访问表单、切换页签并关闭。</td></tr><tr><td>实际授权视频</td><td>1920 × 1080,175.76 秒;预览接口 HTTP 200,视频元数据及画面正常。</td></tr><tr><td>本机与局域网</td><td>本机完成操作;192.168.31.168:6180 入口 HTTP 200。</td></tr><tr><td>构建</td><td>pnpm run build 通过,包含类型检查及模型编辑器生成包校验。</td></tr></table>
<p><a href="results.json">机器验证结果</a></p>
<h2>桌面预览</h2><img src="01-desktop-preview.png" alt="桌面弹窗和视频完整显示">
<h2>矮窗口滚动</h2><img src="02-short-preview.png" alt="矮窗口中固定标题和底部关闭按钮">
<h2>手机宽度</h2><img class="mobile" src="03-mobile-preview.png" alt="窄屏筛选和视频预览">
<h2>暗色主题</h2><img src="04-dark-preview.png" alt="暗色弹窗预览">
<h2>生成表单</h2><img src="05-generation-form.png" alt="生成表单与固定关闭按钮">
<h2>另外观察到的问题</h2><div class="note">首条“数控机床产品讲解”的预览接口返回 404,本轮使用第二条授权视频验证布局,未修复资源文件问题。另外一次进入弹窗前,测试工程保存提示封面生成或上传失败,内容已保存并保留原封面;本轮未修改封面功能。</div>
<h2>测试数据与范围</h2><p>使用账号文件中的内部维护账号及已有测试工程 410。打开数字人服务前,编辑器按既有逻辑保存测试工程。未提交新生成任务、导入绑定或发布。仅调整弹窗布局,不涉及 API 或数据库改动。本轮没有重新验收完整成片生成流程。</p><p>复现脚本:ute2e/tools/narration-dialog-layout.mjs。</p></main></html>

+ 73
- 0
reports/narration-dialog-layout-20260909/results.json ファイルの表示

@@ -0,0 +1,73 @@
{
"passed": true,
"projectId": "410",
"accountType": "内部维护",
"checks": [
{
"label": "01-desktop-preview",
"viewport": {
"width": 1920,
"height": 911
},
"theme": "light",
"result": "PASS",
"video": {
"width": 1920,
"height": 1080,
"duration": 175.76
}
},
{
"label": "02-short-preview",
"viewport": {
"width": 1366,
"height": 600
},
"theme": "light",
"result": "PASS",
"video": {
"width": 1920,
"height": 1080,
"duration": 175.76
}
},
{
"label": "03-mobile-preview",
"viewport": {
"width": 390,
"height": 844
},
"theme": "light",
"result": "PASS",
"video": {
"width": 1920,
"height": 1080,
"duration": 175.76
}
},
{
"label": "04-dark-preview",
"viewport": {
"width": 1366,
"height": 768
},
"theme": "dark",
"result": "PASS",
"video": {
"width": 1920,
"height": 1080,
"duration": 175.76
}
},
{
"label": "生成表单、任务页签与底部关闭",
"result": "PASS"
},
{
"label": "局域网入口",
"result": "PASS"
}
],
"dataChanges": "使用已有测试工程和授权视频。打开数字人服务前编辑器按既有逻辑保存测试工程;未提交生成、导入绑定或发布。",
"observations": ["首条数控机床产品讲解视频预览返回 HTTP 404,改用第二条真实授权视频完成布局验证。", "一次打开弹窗前保存测试工程时出现封面生成或上传失败提示,内容已保存并保留原封面;本轮未修改封面功能。"]
}

バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


+ 18
- 0
reports/open-platform-pdf-20260909/index.html ファイルの表示

@@ -0,0 +1,18 @@
<!doctype html>
<html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>开放 API 文档 PDF 验证报告</title>
<style>body{margin:0;background:#f3f6f5;color:#173b32;font:16px/1.7 system-ui,sans-serif}main{max-width:1100px;margin:32px auto;padding:30px;background:white;border:1px solid #dbe6e0;border-radius:12px}h1{margin-top:0}h2{font-size:21px;margin-top:30px}a{color:#087f5b}table{border-collapse:collapse;width:100%}td,th{border:1px solid #dbe6e0;padding:10px;text-align:left}th{background:#eef5f1}img{max-width:100%;border:1px solid #dbe6e0}small{color:#657c74}.preview{max-width:700px}code{overflow-wrap:anywhere}</style>
<main><h1>开放 API 文档 PDF 验证报告</h1><p>2026-09-09 · 后端生成,页面直接下载 · 验证通过</p>
<p>文档页新增“导出 PDF”。网页与 PDF 共用后端接口说明,输入参数来自 OpenAPI 契约。保留原有契约下载入口。无需打印窗口、外部转换服务或数据库变更。</p>
<p><a href="digital-human-open-api.pdf">查看下载样例(29 页)</a> · <a href="results.json">浏览器测试结果</a></p>
<table><thead><tr><th>检查项</th><th>结果</th></tr></thead><tbody>
<tr><td>匿名文档页加载与实际文件下载</td><td>通过:13 个接口,PDF 文件头、文件名与响应类型正确。</td></tr>
<tr><td>失败提示与重试</td><td>通过:浏览器拦截下载请求模拟 503,显示 message;按钮恢复后可重新下载。</td></tr>
<tr><td>目录滚动联动</td><td>通过:滚动至第 9 个接口时,左侧目录对应选中。</td></tr>
<tr><td>后端自动化测试</td><td>通过:接口覆盖与契约一致;每个接口包含描述、输入、输出、错误码;14 个书签;字体嵌入;缓存返回一致。</td></tr>
<tr><td>前端类型检查</td><td>通过:vue-tsc --noEmit。</td></tr>
<tr><td>PDF 排版检查</td><td>29 页全部渲染检查,中文、表格、示例与页码可读,无内容重叠或裁切。</td></tr>
</tbody></table>
<h2>页面下载</h2><img src="01-pdf-download.png" alt="开放 API 文档页面下载 PDF 成功提示">
<h2>PDF 参数表预览</h2><img class="preview" src="02-pdf-parameters.png" alt="PDF 第 19 页:成片生成任务的输入参数表">
<h2>实施与验证范围</h2><p>后端使用 ReportLab,随包提供获许可的中文字体并嵌入 PDF。新增公开文档数据接口与 PDF 下载接口,仅输出静态说明和占位示例。开发服务已重启并实测;本轮未重复测试数字人业务生成流程,也未部署到生产环境。</p>
<small>复现脚本:ute2e/tools/open-platform-pdf.mjs;后端测试:tests/test_open_api_documentation.py。</small></main></html>

+ 9
- 0
reports/open-platform-pdf-20260909/results.json ファイルの表示

@@ -0,0 +1,9 @@
{
"passed": true,
"endpoints": 13,
"backendPdfDownload": true,
"errorMessage": true,
"retry": true,
"scrollSync": true,
"scriptErrors": []
}

バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


+ 28
- 0
reports/open-platform-style-20260909/index.html ファイルの表示

@@ -0,0 +1,28 @@
<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>开放应用与 API 文档调整验收 · 2026-09-09</title>
<style>
*{box-sizing:border-box}body{margin:0;background:#f3f6f5;color:#172b26;font:15px/1.75 system-ui,"Microsoft YaHei",sans-serif}main{max-width:1250px;margin:auto;padding:32px 24px}header,section{background:#fff;border:1px solid #dce5e0;border-radius:10px;padding:26px;margin-bottom:22px}h1{font-size:26px;margin:0 0 8px}h2{font-size:20px;margin:0 0 16px}h3{font-size:16px}p{margin:10px 0}.muted{color:#586d66}.status{display:inline-block;border-radius:20px;background:#e0f4e9;color:#08744d;padding:3px 13px;font-weight:600}.metrics{display:flex;flex-wrap:wrap;gap:20px;margin-top:20px}.metrics div{padding:12px 20px;background:#f5f9f7;border-radius:6px}.metrics b{display:block;color:#087b54;font-size:25px}.grid{display:grid;grid-template-columns:1fr 1fr;gap:20px}.shot{min-width:0;margin:0;border:1px solid #dce5e0;border-radius:6px;overflow:hidden}.shot img{width:100%;display:block}.shot figcaption{padding:10px 14px;color:#586d66;font-size:13px}.mobile img{max-width:390px;margin:auto}.wide{grid-column:1/-1}.full-doc{max-width:820px;margin:auto}.full-doc img{width:100%}a{color:#087b54}table{width:100%;border-collapse:collapse}th,td{text-align:left;border:1px solid #dce5e0;padding:10px 12px}th{background:#f4f8f6}code{overflow-wrap:anywhere;background:#f4f6f5;padding:2px 4px}li+li{margin-top:5px}@media(max-width:760px){main{padding:12px}header,section{padding:17px}.grid{grid-template-columns:1fr}h1{font-size:22px}.metrics{gap:8px}.metrics div{padding:10px}}
</style></head>
<body><main>
<header><span class="status">验收通过</span><h1>开放应用与 API 文档调整</h1><p class="muted">数字人服务系统 · 2026-09-09 · 开发环境真实页面验证</p><p>开放应用统一使用现有系统的标题、搜索工具栏、列表和底部分页。API 文档从开放应用页内进入,采用目录导航与完整正文,每个接口直接展示描述、输入参数、输出参数、返回示例和错误码。</p><div class="metrics"><div><b>13 / 13</b>业务接口完整展示</div><div><b>8</b>浏览器检查组通过</div><div><b>33</b>相关后端测试通过</div><div><b>6</b>真实数据库搜索检查通过</div></div></header>
<section><h2>本次变更与验证</h2><table><thead><tr><th>项目</th><th>结果</th></tr></thead><tbody>
<tr><td>页面风格</td><td>使用统一语义色、系统页面容器、Element Plus 表格、工具按钮和分页;浅色、深色均截图检查。</td></tr>
<tr><td>文档入口</td><td>菜单仅保留开放应用,右上角按钮进入文档二级页;当前菜单继续高亮开放应用,返回按钮可正常回到列表。</td></tr>
<tr><td>文档内容</td><td>13 个接口与运行中的 OpenAPI 契约逐项比对;无折叠组件,每个接口均包含四个章节和返回示例。参数类型、默认值与范围取自服务端契约。</td></tr>
<tr><td>参数与错误码</td><td>明确请求头、路径、查询和 JSON 参数;注明幂等键必填、视频尺寸范围、授权要求;区分 HTTP 业务码、任务失败码和二进制响应。</td></tr>
<tr><td>移动端与外部访问</td><td>公共文档 /developer 无需登录;390px 窄屏无页面横向溢出,宽参数表在表格内滚动。</td></tr>
<tr><td>附带修复</td><td>KingBase MySQL 兼容模式下,原 contains 查询中的 || 被解释为逻辑运算,造成搜索返回全部数据。改为绑定完整 LIKE 参数,并转义通配符;应用名称、APPID 和授权资源搜索验证通过。</td></tr>
</tbody></table><p>前端 <code>pnpm exec vue-tsc --noEmit</code> 通过。后端菜单测试 5 项、迁移测试 23 项、开放平台测试 5 项通过;后者包含新增关键词和字面通配符回归用例。真实 KingBase 对应用及五类资源的不匹配关键词均返回 0 条。浏览器无未捕获脚本错误。</p><p>数据库迁移 <code>20260909_034_open_api_docs_secondary.sql</code> 已在开发库执行并复核:软删除文档菜单及其权限关联,保留访问权限本身;恢复默认菜单不会重新出现文档。全量 DDL 和接口说明已同步。</p><p class="muted">环境说明:测试期间发现开发配置使用 2 个数据库连接且无溢出连接,后台任务和页面并发曾触发连接池等待。本次运行进程按代码默认值使用 pool_size=10、max_overflow=20,未修改持久化连接配置。本轮未重复视频生成业务测试,也未更改应用密钥、授权数据或成片协议。</p><p><a href="results.json">浏览器检查结果 JSON</a> · <a href="../../tools/open-platform-style.mjs">可复跑的 E2E 脚本</a></p></section>
<section><h2>页面截图</h2><p class="muted">点击截图可查看原图。</p><div class="grid">
<figure class="shot wide"><a href="01-applications-light.png"><img src="01-applications-light.png" alt="浅色开放应用列表,统一工具栏和分页"></a><figcaption>开放应用:标题、搜索、重置、新建、列表和底部分页统一。</figcaption></figure>
<figure class="shot wide"><a href="02-docs-overview-light.png"><img src="02-docs-overview-light.png" alt="文档二级页面,系统菜单仅显示开放应用"></a><figcaption>文档二级页:左侧系统菜单不展示文档,页内目录提供接口定位。</figcaption></figure>
<figure class="shot wide"><a href="09-generation-input.png"><img src="09-generation-input.png" alt="成片生成接口描述和输入参数"></a><figcaption>接口正文:请求方式、路径、权限和输入参数表直接呈现。</figcaption></figure>
<figure class="shot"><a href="04-docs-dark.png"><img src="04-docs-dark.png" alt="深色文档页"></a><figcaption>文档深色主题。</figcaption></figure>
<figure class="shot"><a href="05-applications-dark.png"><img src="05-applications-dark.png" alt="深色开放应用列表"></a><figcaption>开放应用深色主题。</figcaption></figure>
<figure class="shot wide"><a href="06-application-dialog-dark.png"><img src="06-application-dialog-dark.png" alt="深色应用配置弹窗"></a><figcaption>配置弹窗沿用系统主题,授权资源区域正常显示;测试未保存修改。</figcaption></figure>
<figure class="shot mobile"><a href="07-docs-mobile.png"><img src="07-docs-mobile.png" alt="390像素移动端公共文档入口"></a><figcaption>移动端:公共文档与目录。</figcaption></figure>
<figure class="shot mobile"><a href="08-docs-mobile-parameters.png"><img src="08-docs-mobile-parameters.png" alt="移动端参数表,表格内部横向滚动"></a><figcaption>移动端:宽参数表内部滚动,页面本身不溢出。</figcaption></figure>
</div></section>
<section><h2>单个接口完整展示</h2><p>以“提交成片生成任务”为例,以下完整截图覆盖描述、输入参数、输出参数、返回示例和错误码。</p><figure class="shot full-doc"><a href="03-generation-document.png"><img src="03-generation-document.png" alt="成片生成接口四个章节完整截图"></a></figure></section>
</main></body></html>

バイナリ
ファイルの表示


+ 16
- 0
reports/open-platform-style-20260909/results.json ファイルの表示

@@ -0,0 +1,16 @@
{
"passed": true,
"operationCount": 13,
"checks": [
"服务端菜单移除文档,保留开放应用",
"应用搜索、重置、分页区域正常",
"文档作为二级页,开放应用菜单正确高亮",
"13 个接口与服务端契约一致,均直接展示四个章节和返回示例",
"目录定位、生成幂等请求头必填和默认尺寸参数正确",
"浅色与深色页面及配置弹窗正常,返回开放应用正常",
"公共文档无需登录,390px 窄屏无页面横向溢出",
"浏览器无未捕获脚本错误"
],
"errors": [],
"timestamp": "2026-09-09T00:44:37.634Z"
}

バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


+ 3126
- 0
reports/training-step-layout-20260908/after.json
ファイル差分が大きすぎるため省略します
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


バイナリ
ファイルの表示


+ 2083
- 0
reports/training-step-layout-20260908/before.json
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 12
- 0
reports/training-step-layout-20260908/index.html ファイルの表示

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>训练步骤布局修复与讲解来源核查 · 2026-09-08</title>
<style>body{margin:32px auto;padding:0 24px;max-width:1200px;font:16px/1.7 system-ui,sans-serif;color:#183b43;background:#f4f8f8}h1{font-size:26px}section{padding:20px;margin:18px 0;border:1px solid #cadddd;border-radius:10px;background:white}table{border-collapse:collapse;width:100%}th,td{border:1px solid #cadddd;padding:10px;text-align:left}a{color:#087e75}img{display:block;width:100%;border:1px solid #cadada}code{overflow-wrap:anywhere}.comparison{display:grid;grid-template-columns:1fr 1fr;gap:16px}@media(max-width:800px){.comparison{grid-template-columns:1fr}}</style>
<h1>训练步骤布局修复与讲解来源核查</h1><p>2026-09-08 · 正式 Web 6180 · 训练编排基础编排入口</p>
<section><h2>修复结果:通过</h2><p>复现项目:ZDS-虚拟仿真1(409)。较矮视口下,步骤网格压缩至 82px,内容需要约 92px,标签越过底边 1px。修复为按内容高度分配网格行,标签允许换行,不裁切内容、不隐藏信息。</p><p>变更仅 1 个 CSS 文件、4 行:<code>src/features/guide-legacy/demo-src/training-editor.css</code>。未修改讲解来源逻辑,未提交 Git。</p>
<table><tr><th>验证范围</th><th>结果</th></tr><tr><td>1920×1080、1920×900、1440×900、1440×768</td><td>每个视口检查 8 个步骤,标签最小底部留白均为 9px</td></tr><tr><td>1440×900 暗色与跟随系统暗色的 CSS 渲染</td><td>8 个步骤均通过;未修改用户服务端主题偏好</td></tr><tr><td>列表滚动、末步点击与选中状态</td><td>通过</td></tr><tr><td>pnpm run build(含 TypeScript 与 iframe 产物校验)</td><td>通过;保留既有 chunk 大小提醒</td></tr><tr><td>业务数据</td><td>管理员只读会话;0 次内容写请求,没有创建、保存或发布测试项目,会话已退出</td></tr></table><p>证据:<a href="before.json">修复前测量</a> · <a href="after.json">修复后测量</a>。修复前 JSON 的 PASS 表示采集成功,越界数值记录在 card/badges 坐标中;修复后另有标签留白断言。</p></section>
<section><h2>1920×900 修复前后</h2><div class="comparison"><div><p>修复前:标签跨越卡片底边</p><a href="before-1920x900.png"><img src="before-1920x900.png" alt="修复前的步骤列表"></a></div><div><p>修复后:底部留白 9px</p><a href="after-1920x900.png"><img src="after-1920x900.png" alt="修复后的步骤列表"></a></div></div><p><a href="after-1440x768.png">1440×768</a> · <a href="after-1440x900-dark.png">暗色</a> · <a href="after-1440x900-system-dark.png">跟随系统暗色</a> · <a href="after-last-step.png">滚动选择最后一步</a></p></section>
<section><h2>提交 3130346</h2><p>作者与提交者均为 zhuds(zhudasen@126.com);时间为 2026-09-06 23:20:30 +08:00;标题为“feat:样式调整”。</p><table><tr><th>文件</th><th>改动</th></tr><tr><td>src/styles/index.scss</td><td>滚动容器 wrap 改用 flex 传递高度,route-view-content 增加 flex: 1 0 auto,支持长页面正确触发滚动条。</td></tr><tr><td>src/styles/integration.scss</td><td>维修知识页桌面增加 contain: size,防止内容反向撑高外层;1000px 及以下解除该限制。</td></tr></table></section>
<section><h2>讲解设置来源:当前仍是内置演示逻辑</h2><p>核查实际挂载入口 <code>LegacyTrainingPageView.vue:268</code>、<code>training-editor.js</code> 的目录、渲染、生成和试听方法,以及 <code>training-legacy/bridge.ts</code> 保存映射。宿主注入场景列表、场景加载、保存与发布回调,没有覆盖数字人目录或生成函数。</p>
<table><tr><th>界面项</th><th>实际来源与行为</th><th>代码位置</th></tr><tr><td>数字人资源库</td><td>3 个硬编码目录、5 段站点静态 MP4;另将当前训练项目步骤中已有 generatedVideoId/generatedVideoUrl 汇总为“我的作品”,不是从数字人平台作品库查询。</td><td>training-editor.js:68、1243</td></tr><tr><td>形象、声音、背景</td><td>各 3 项硬编码选项。形象与背景是 public/images 中的静态图片。</td><td>training-editor.js:98、104、110</td></tr><tr><td>现场生成</td><td>检查必填项后,在前端生成时间戳 ID,将固定 musetalk15_teacher-m_kb-script-18s.mp4 写入步骤;没有提交真实生成任务,文稿不会改变该视频。</td><td>training-editor.js:226、1598</td></tr><tr><td>“已同步到作品集”</td><td>仅更新当前项目步骤元数据并标记未保存,“我的作品”从这些步骤汇总;没有向数字人系统创建作品的请求。</td><td>training-editor.js:1243、1613</td></tr><tr><td>“数字人系统已连接”</td><td>直接渲染的固定文案,没有根据健康检查或连接接口返回值判断。</td><td>training-editor.js:1346、1376</td></tr><tr><td>声音试听</td><td>调用 previewSoftChime(640) 提示音;不是对应声音的真实试听片段。</td><td>training-editor.js:1633</td></tr></table>
<p>静态素材位置:<code>unreal_tran_web/public/video/digital-instructor-v3/clips/</code>,固定生成示例位于 <code>examples/musetalk15_teacher-m_kb-script-18s.mp4</code>。对 5 段目录视频和 1 段生成示例逐个 HEAD 检查,均返回 HTTP 200、video/mp4。</p><p>本次仅核查这些入口的实现,没有调用生成按钮或变更绑定。真正接入属于后续功能调整:建议资源库通过数字车间后端查询数字人平台可用作品;现场生成提交真实任务,显示进度与失败状态,完成后绑定受控视频资源。“已连接”和“已同步”须由真实接口结果驱动,等待产品确认后实施。</p></section></html>

+ 75
- 0
tools/advanced-drag-layout.mjs ファイルの表示

@@ -0,0 +1,75 @@
import fs from 'node:fs'
import path from 'node:path'
import assert from 'node:assert/strict'
import { createRequire } from 'node:module'
import { fileURLToPath } from 'node:url'
import { login, call } from './role-permissions-live.mjs'
const require = createRequire(new URL('../package.json', import.meta.url))
const { chromium, expect } = require('@playwright/test')
const report = fileURLToPath(new URL('../reports/advanced-drag-layout-20260909/', import.meta.url))
fs.mkdirSync(report, { recursive: true })
const session = await login('root')
const project = (await call('tran/v1/content/projects?type=TRAINING&size=100', session.accessToken)).data.records.find(row => row.name.includes('APE2E-数字人讲解-狐狸'))
assert.ok(project)
const browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--enable-unsafe-swiftshader'] })
let page
const checks = []
try {
const context = await browser.newContext({ viewport: { width: 1920, height: 911 } })
await context.addInitScript(({ accessToken, refreshToken }) => {
sessionStorage.setItem('unreal-tran:web:access-token:v1', accessToken)
sessionStorage.setItem('unreal-tran:web:refresh-token:v1', refreshToken)
localStorage.setItem('unreal-tran:web:preferences:v1', JSON.stringify({ themeMode: 'military' }))
}, session)
page = await context.newPage()
await page.goto(`http://127.0.0.1:6180/content/training-projects/${project.id}/legacy`)
await page.getByRole('radio', { name: '高级编排', exact: true }).click({ timeout: 60000 })
const canvas = page.locator('[data-role="flow-canvas"]')
const node = page.locator('[data-flow-node="flow-start"]')
const head = node.locator('.tfa-node-head')
await head.scrollIntoViewIfNeeded()
const initial = await node.evaluate(el => ({ x: parseFloat(el.style.left), y: parseFloat(el.style.top) }))
const box = await head.boundingBox(), area = await canvas.boundingBox()
await page.mouse.move(box.x + 45, box.y + 22)
await page.mouse.down()
const pointerY = area.y + area.height - 100
await page.mouse.move(box.x + 45, pointerY, { steps: 12 })
const before = await node.evaluate(el => parseFloat(el.style.top))
const scrollDelta = await canvas.evaluate(el => { const old = el.scrollTop; el.scrollTop += 180; return el.scrollTop - old })
assert.ok(scrollDelta > 80)
await page.mouse.move(box.x + 45, pointerY + 5, { steps: 2 })
const after = await node.evaluate(el => parseFloat(el.style.top))
assert.ok(Math.abs(after - before - scrollDelta - 5) < 3, 'Node follows pointer in scrolled coordinates')
const oldLimit = await canvas.evaluate(el => el.clientHeight - 110)
assert.ok(after > oldLimit + 80, 'Node reaches grid below the former viewport limit')
await page.mouse.up()
checks.push({ name: '拖入下方网格,拖动中滚动不跳位', result: 'PASS', before, after, scrollDelta, formerLimit: oldLimit })
const scrollBefore = await canvas.evaluate(el => el.scrollTop)
await head.click()
await expect.poll(() => canvas.evaluate(el => el.scrollTop)).toBe(scrollBefore)
await expect.poll(() => node.evaluate(el => parseFloat(el.style.top))).toBe(after)
await page.screenshot({ path: path.join(report, '01-lower-grid.png'), animations: 'disabled' })
checks.push({ name: '选择节点后保留画布滚动位置', result: 'PASS' })
await page.setViewportSize({ width: 1366, height: 768 })
const last = page.locator('.tfa-node').last(), lastHead = last.locator('.tfa-node-head')
await lastHead.scrollIntoViewIfNeeded()
const previous = await last.evaluate(el => ({ x: parseFloat(el.style.left), y: parseFloat(el.style.top) }))
const lastBox = await lastHead.boundingBox()
await page.mouse.move(lastBox.x + 45, lastBox.y + 22)
await page.mouse.down()
await page.mouse.move(lastBox.x + 55, lastBox.y + 32, { steps: 4 })
await page.mouse.up()
const next = await last.evaluate(el => ({ x: parseFloat(el.style.left), y: parseFloat(el.style.top) }))
assert.ok(Math.abs(next.x - previous.x - 10) < 2 && Math.abs(next.y - previous.y - 10) < 2, 'Small drag after horizontal scroll must not jump to viewport limit')
await page.screenshot({ path: path.join(report, '02-horizontal-scroll.png'), animations: 'disabled' })
checks.push({ name: '窄窗口横向滚动后拖动不跳位', result: 'PASS', previous, next })
await page.getByRole('radio', { name: '基础编排', exact: true }).click()
await page.getByRole('radio', { name: '高级编排', exact: true }).click()
assert.equal(await node.evaluate(el => parseFloat(el.style.top)), after)
checks.push({ name: '切换编排模式保留节点坐标', result: 'PASS' })
fs.writeFileSync(path.join(report, 'results.json'), JSON.stringify({ passed: true, projectId: project.id, accountType: '内部维护', initial, checks, dataChanges: '仅调整已有测试工程的浏览器草稿,未点击保存或发布。' }, null, 2))
console.log('Advanced drag layout checks passed')
} catch (error) {
if (page) await page.screenshot({ path: path.join(report, 'failure.png'), animations: 'disabled' }).catch(() => {})
throw error
} finally { await browser.close() }

+ 79
- 0
tools/narration-dialog-layout.mjs ファイルの表示

@@ -0,0 +1,79 @@
import fs from 'node:fs'
import path from 'node:path'
import assert from 'node:assert/strict'
import { createRequire } from 'node:module'
import { fileURLToPath } from 'node:url'
import { login, call } from './role-permissions-live.mjs'
const require = createRequire(new URL('../package.json', import.meta.url))
const { chromium, expect } = require('@playwright/test')
const report = fileURLToPath(new URL('../reports/narration-dialog-layout-20260909/', import.meta.url))
fs.mkdirSync(report, { recursive: true })
const session = await login('root')
const project = (await call('tran/v1/content/projects?type=TRAINING&size=100', session.accessToken)).data.records.find(row => row.name.includes('APE2E-数字人讲解-狐狸'))
assert.ok(project, 'Existing narration test project available')
const browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--enable-unsafe-swiftshader'] })
const checks = []
let page
try {
const context = await browser.newContext({ viewport: { width: 1920, height: 911 } })
await context.addInitScript(({ accessToken, refreshToken }) => {
sessionStorage.setItem('unreal-tran:web:access-token:v1', accessToken)
sessionStorage.setItem('unreal-tran:web:refresh-token:v1', refreshToken)
}, session)
page = await context.newPage()
await page.goto(`http://127.0.0.1:6180/content/training-projects/${project.id}/legacy`)
await page.locator('[data-action="set-tab"]').filter({ hasText: '讲解' }).click({ timeout: 60000 })
await page.locator('[data-action="set-narration-source"][data-source="digital-library"]').click()
await page.locator('[data-action="manage-digital-human"]').click()
const dialog = page.locator('.digital-human-narration-dialog')
console.log('Narration dialog opened')
await expect(dialog.locator('.connection-state')).toContainText('已连接数字人服务', { timeout: 30000 })
const preview = dialog.getByRole('button', { name: '预览', exact: true }).last()
const previewResponse = page.waitForResponse(response => response.url().includes('/digital-human/videos/') && response.url().endsWith('/preview'), { timeout: 125000 })
await preview.click({ timeout: 30000 })
console.log('Authorized video preview requested')
assert.equal((await previewResponse).status(), 200, 'Authorized video preview response')
await page.waitForFunction(() => document.querySelector('.video-preview video')?.readyState >= 1, null, { timeout: 20000 })
await dialog.locator('video').evaluate(video => { video.currentTime = 0.1 })
async function verify(label, width, height, theme = 'light') {
await page.setViewportSize({ width, height })
await page.evaluate(async value => {
const { usePreferencesStore } = await import('/src/stores/preferences.ts')
usePreferencesStore().setThemeMode(value === 'dark' ? 'dark' : 'military')
}, theme)
await expect.poll(async () => dialog.evaluate(el => {
const box = el.getBoundingClientRect(), footer = el.querySelector('.el-dialog__footer').getBoundingClientRect()
const body = el.querySelector('.el-dialog__body')
return box.top >= 0 && box.bottom <= innerHeight + 1 && box.left >= 0 && box.right <= innerWidth + 1 && footer.bottom <= innerHeight && body.scrollWidth <= body.clientWidth + 1
})).toBe(true)
await dialog.locator('.video-preview').scrollIntoViewIfNeeded()
await expect(dialog.locator('.el-dialog__footer').getByRole('button', { name: '关闭', exact: true })).toBeVisible()
const video = await dialog.locator('video').evaluate(el => ({ width: el.videoWidth, height: el.videoHeight, duration: el.duration }))
assert.ok(video.width > 0 && video.duration > 0)
await page.screenshot({ path: path.join(report, `${label}.png`), animations: 'disabled' })
checks.push({ label, viewport: { width, height }, theme, result: 'PASS', video })
}
await verify('01-desktop-preview', 1920, 911)
await verify('02-short-preview', 1366, 600)
await verify('03-mobile-preview', 390, 844)
await verify('04-dark-preview', 1366, 768, 'dark')
await dialog.getByRole('tab', { name: '现场生成成片' }).click()
await expect(dialog.getByRole('button', { name: '提交生成', exact: true })).toBeAttached()
await dialog.getByRole('button', { name: '提交生成', exact: true }).scrollIntoViewIfNeeded()
await page.screenshot({ path: path.join(report, '05-generation-form.png'), animations: 'disabled' })
await dialog.getByRole('tab', { name: '生成任务', exact: true }).click()
await dialog.locator('.el-dialog__footer').getByRole('button', { name: '关闭', exact: true }).click()
await expect(dialog).toBeHidden()
checks.push({ label: '生成表单、任务页签与底部关闭', result: 'PASS' })
const lan = await context.request.get('http://192.168.31.168:6180/')
assert.equal(lan.status(), 200)
checks.push({ label: '局域网入口', result: 'PASS' })
fs.writeFileSync(path.join(report, 'results.json'), JSON.stringify({ passed: true, projectId: project.id, accountType: '内部维护', checks, dataChanges: '使用已有测试工程和授权视频。打开数字人服务前编辑器按既有逻辑保存测试工程;未提交生成、导入绑定或发布。' }, null, 2))
console.log('Narration dialog layout checks passed')
} catch (error) {
if (page) {
await page.screenshot({ path: path.join(report, 'failure.png'), animations: 'disabled' }).catch(() => {})
console.error(await page.locator('.el-message').allTextContents())
}
throw error
} finally { await browser.close() }

+ 40
- 0
tools/open-platform-pdf.mjs ファイルの表示

@@ -0,0 +1,40 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
import assert from 'node:assert/strict'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..')
const require = createRequire(path.join(root, 'ai_person/ape2e/package.json'))
const { chromium, expect } = require('@playwright/test')
const report = path.join(root, 'unreal_tran/ute2e/reports/open-platform-pdf-20260909')
fs.mkdirSync(report, { recursive: true })
const browser = await chromium.launch({ channel: 'msedge', headless: true })
try {
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } })
const errors = []
page.on('pageerror', error => errors.push(error.message))
await page.goto('http://127.0.0.1:8003/developer')
await expect(page.locator('.endpoint-section')).toHaveCount(13)
const button = page.getByRole('button', { name: '导出 PDF', exact: true })
const downloadPromise = page.waitForEvent('download')
await button.click()
const download = await downloadPromise
assert.equal(download.suggestedFilename(), 'digital-human-open-api.pdf')
await download.saveAs(path.join(report, 'digital-human-open-api.pdf'))
assert.equal(fs.readFileSync(path.join(report, 'digital-human-open-api.pdf')).subarray(0, 5).toString(), '%PDF-')
await expect(button).toBeEnabled()
await page.screenshot({ path: path.join(report, '01-pdf-download.png'), animations: 'disabled' })
await page.route('**/open-api/v1/documentation.pdf', route => route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ code: 50000, message: '导出服务暂不可用', data: null }) }))
await button.click()
await expect(page.locator('.el-message--error')).toBeVisible()
await expect(button).toBeEnabled()
await page.unroute('**/open-api/v1/documentation.pdf')
const retry = page.waitForEvent('download')
await button.click()
assert.equal((await retry).suggestedFilename(), 'digital-human-open-api.pdf')
await page.locator('#endpoint-9').evaluate(el => window.scrollTo({ top: window.scrollY + el.getBoundingClientRect().top - 20, behavior: 'instant' }))
await expect(page.locator('.docs-nav a.active')).toHaveAttribute('href', '#endpoint-9')
assert.deepEqual(errors, [])
fs.writeFileSync(path.join(report, 'results.json'), JSON.stringify({ passed: true, endpoints: 13, backendPdfDownload: true, errorMessage: true, retry: true, scrollSync: true, scriptErrors: errors }, null, 2))
console.log('PDF download, error message, retry and scroll sync passed')
} finally { await browser.close() }

+ 98
- 0
tools/open-platform-style.mjs ファイルの表示

@@ -0,0 +1,98 @@
import fs from 'node:fs'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import assert from 'node:assert/strict'
import { createRequire } from 'node:module'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..')
const require = createRequire(path.join(root, 'ai_person/ape2e/package.json'))
const { chromium, expect: baseExpect } = require('@playwright/test')
const expect = baseExpect.configure({ timeout: 25000 })
const report = path.join(root, 'unreal_tran/ute2e/reports/open-platform-style-20260909')
fs.mkdirSync(report, { recursive: true })
const values = new Map()
for (const line of fs.readFileSync(path.join(root, 'ai_person/账号.MD'), 'utf8').split(/\r?\n/)) {
const match = line.match(/^\s*([^::]+)\s*[::]\s*(.*?)\s*$/)
if (match?.[1] && match[2]) values.set(match[1].trim(), match[2].trim())
}
const base = 'http://127.0.0.1:8003'
const response = await fetch(base + '/api/auth/v1/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: values.get('账号') || values.get('用户名'), password: values.get('密码'), roleCode: 'admin' }) })
const payload = await response.json()
assert.equal(payload.code, 0, 'login must succeed')
const token = payload.data.accessToken || payload.data.tokens?.accessToken
const nav = await fetch(base + '/api/auth/v1/menus/navigation', { headers: { Authorization: 'Bearer ' + token } }).then(r => r.json())
assert.equal(nav.code, 0)
assert(!JSON.stringify(nav.data).includes('settings-open-api-docs'), 'docs must not be in server menu')
assert(JSON.stringify(nav.data).includes('settings-open-applications'))
const schema = await fetch(base + '/open-api/v1/openapi.json').then(r => r.json())
const operationCount = Object.values(schema.paths).reduce((n, methods) => n + Object.keys(methods).length, 0)
const checks = ['服务端菜单移除文档,保留开放应用']
const browser = await chromium.launch({ channel: 'msedge', headless: true })
const errors = []
try {
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } })
page.on('pageerror', error => errors.push(error.message))
await page.goto(base + '/login')
await page.evaluate(token => sessionStorage.setItem('ai-person:web:access-token:v1', token), token)
await page.goto(base + '/settings/open-applications')
await page.getByText('数字车间开发联调', { exact: true }).waitFor()
await expect(page.locator('#main-sidebar')).not.toContainText('开放 API 文档')
const search = page.getByPlaceholder('搜索应用名称或 APPID')
const searchResponse = page.waitForResponse(response => response.url().includes('open-applications?keyword=%') && response.request().method() === 'GET')
await search.fill('数字车间开发联调')
await page.getByRole('button', { name: '搜索', exact: true }).click()
assert.equal((await (await searchResponse).json()).data.total, 1)
await expect(page.locator('.applications-table .el-table__row')).toHaveCount(1)
await page.getByRole('button', { name: '重置', exact: true }).click()
await expect(search).toHaveValue('')
await expect(page.locator('.applications-table .el-table__row')).toHaveCount(2)
checks.push('应用搜索、重置、分页区域正常')
await page.screenshot({ path: path.join(report, '01-applications-light.png') })
await page.getByRole('button', { name: '开放 API 文档', exact: true }).click()
await expect(page).toHaveURL(/settings\/open-api-docs/)
await expect(page.locator('.endpoint-section')).toHaveCount(operationCount)
assert.equal(operationCount, 13)
assert.equal(await page.locator('.el-collapse').count(), 0)
for (const heading of ['1. 接口描述', '2. 输入参数', '3. 输出参数', '4. 错误码']) {
await expect(page.locator('.endpoint-section').getByRole('heading', { name: heading, exact: true })).toHaveCount(13)
}
await expect(page.locator('#main-sidebar')).not.toContainText('开放 API 文档')
await expect(page.locator('#main-sidebar .el-menu-item.is-active')).toContainText('开放应用')
checks.push('文档作为二级页,开放应用菜单正确高亮', '13 个接口与服务端契约一致,均直接展示四个章节和返回示例')
await page.screenshot({ path: path.join(report, '02-docs-overview-light.png') })
await page.locator('.docs-nav a[href="#endpoint-9"]').click()
await expect(page.locator('#endpoint-9')).toBeInViewport()
await page.screenshot({ path: path.join(report, '09-generation-input.png') })
await page.locator('#endpoint-9').screenshot({ path: path.join(report, '03-generation-document.png') })
const idem = page.locator('#endpoint-9 .parameter-table').first().getByRole('row').filter({ hasText: 'Idempotency-Key' })
await expect(idem).toContainText('是')
await expect(page.locator('#endpoint-9')).toContainText('默认值:1280')
checks.push('目录定位、生成幂等请求头必填和默认尺寸参数正确')
await page.getByRole('button', { name: '切换到深色主题' }).click()
await page.getByRole('link', { name: '接入说明', exact: true }).click()
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark')
await page.screenshot({ path: path.join(report, '04-docs-dark.png') })
await page.getByRole('button', { name: '返回开放应用', exact: true }).click()
await page.getByText('数字车间开发联调', { exact: true }).waitFor()
await page.screenshot({ path: path.join(report, '05-applications-dark.png') })
await page.locator('tr').filter({ hasText: '数字车间开发联调' }).getByRole('button', { name: '配置', exact: true }).click()
await expect(page.getByRole('dialog')).toBeVisible()
await expect(page.getByRole('dialog').getByText('授权资源', { exact: true })).toBeVisible()
await page.screenshot({ path: path.join(report, '06-application-dialog-dark.png') })
await page.getByRole('dialog').getByRole('button', { name: '取消', exact: true }).click()
checks.push('浅色与深色页面及配置弹窗正常,返回开放应用正常')
const publicPage = await browser.newPage({ viewport: { width: 390, height: 844 } })
publicPage.on('pageerror', error => errors.push(error.message))
await publicPage.goto(base + '/developer')
await expect(publicPage.locator('.endpoint-section')).toHaveCount(13)
await expect(publicPage.getByRole('button', { name: '返回开放应用' })).toHaveCount(0)
const dimensions = await publicPage.evaluate(() => ({ width: document.documentElement.clientWidth, scroll: document.documentElement.scrollWidth }))
assert(dimensions.scroll <= dimensions.width + 1, `mobile overflow: ${JSON.stringify(dimensions)}`)
await publicPage.screenshot({ path: path.join(report, '07-docs-mobile.png') })
await publicPage.locator('#endpoint-1').scrollIntoViewIfNeeded()
await publicPage.screenshot({ path: path.join(report, '08-docs-mobile-parameters.png') })
checks.push('公共文档无需登录,390px 窄屏无页面横向溢出')
assert.deepEqual(errors, [])
checks.push('浏览器无未捕获脚本错误')
fs.writeFileSync(path.join(report, 'results.json'), JSON.stringify({ passed: true, operationCount, checks, errors, timestamp: new Date().toISOString() }, null, 2))
console.log(JSON.stringify({ passed: true, operationCount, checks: checks.length, report }))
} finally { await browser.close() }

読み込み中…
キャンセル
保存