Просмотр исходного кода

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

main
leiyun 1 неделю назад
Родитель
Сommit
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() }

Загрузка…
Отмена
Сохранить