diff --git a/api-error-messages.config.ts b/api-error-messages.config.ts
new file mode 100644
index 0000000..4ec9e8f
--- /dev/null
+++ b/api-error-messages.config.ts
@@ -0,0 +1,26 @@
+import { defineConfig } from '@playwright/test'
+
+// Real Vue/Element Plus in Edge; every /api/ request is intercepted by the spec.
+export default defineConfig({
+ testDir: './tests',
+ testMatch: 'api-error-messages.spec.ts',
+ outputDir: './reports/api-error-messages-20260906/test-results',
+ fullyParallel: false,
+ workers: 1,
+ retries: 0,
+ timeout: 60_000,
+ expect: { timeout: 12_000 },
+ reporter: [
+ ['list'],
+ ['html', { outputFolder: 'reports/api-error-messages-20260906/playwright', open: 'never' }],
+ ['json', { outputFile: 'reports/api-error-messages-20260906/results.json' }],
+ ],
+ use: {
+ baseURL: 'http://127.0.0.1:6180',
+ browserName: 'chromium', channel: 'msedge', headless: true,
+ locale: 'zh-CN', timezoneId: 'Asia/Shanghai',
+ viewport: { width: 1440, height: 1000 },
+ actionTimeout: 12_000, navigationTimeout: 35_000,
+ screenshot: 'only-on-failure', trace: 'off', video: 'off',
+ },
+})
diff --git a/reports/api-error-messages-20260906/authentication/html/data/071e42a9c4b1b61b2a068c87f745fbde764b7e62.png b/reports/api-error-messages-20260906/authentication/html/data/071e42a9c4b1b61b2a068c87f745fbde764b7e62.png
new file mode 100644
index 0000000..0d864ce
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/html/data/071e42a9c4b1b61b2a068c87f745fbde764b7e62.png differ
diff --git a/reports/api-error-messages-20260906/authentication/html/data/9292f5c4d6b2dfd83de339568ce711babd89154e.md b/reports/api-error-messages-20260906/authentication/html/data/9292f5c4d6b2dfd83de339568ce711babd89154e.md
new file mode 100644
index 0000000..5ad12ed
--- /dev/null
+++ b/reports/api-error-messages-20260906/authentication/html/data/9292f5c4d6b2dfd83de339568ce711babd89154e.md
@@ -0,0 +1,10 @@
+# Page snapshot
+
+```yaml
+- status [ref=e3]:
+ - generic [ref=e4]:
+ - strong [ref=e7]: 装备数字车间
+ - generic [ref=e10]:
+ - generic [ref=e11]: 正在进入系统
+ - paragraph [ref=e12]: 正在验证登录状态并加载工作台,请稍候…
+```
\ No newline at end of file
diff --git a/reports/api-error-messages-20260906/authentication/html/data/9599d34967a3fef35e0bb2b1686dca14e940d437.png b/reports/api-error-messages-20260906/authentication/html/data/9599d34967a3fef35e0bb2b1686dca14e940d437.png
new file mode 100644
index 0000000..723a12d
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/html/data/9599d34967a3fef35e0bb2b1686dca14e940d437.png differ
diff --git a/reports/api-error-messages-20260906/authentication/html/data/acdc2bb53abb72efa451d16cc43612265dee61f5.png b/reports/api-error-messages-20260906/authentication/html/data/acdc2bb53abb72efa451d16cc43612265dee61f5.png
new file mode 100644
index 0000000..5bc0d6b
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/html/data/acdc2bb53abb72efa451d16cc43612265dee61f5.png differ
diff --git a/reports/api-error-messages-20260906/authentication/html/data/e1c6cff12a63fad65e74a58d6db58e175a0452da.png b/reports/api-error-messages-20260906/authentication/html/data/e1c6cff12a63fad65e74a58d6db58e175a0452da.png
new file mode 100644
index 0000000..4f0f49e
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/html/data/e1c6cff12a63fad65e74a58d6db58e175a0452da.png differ
diff --git a/reports/api-error-messages-20260906/authentication/html/data/ee4a5843da13d74b3518c8ba9b9eb38087378136.png b/reports/api-error-messages-20260906/authentication/html/data/ee4a5843da13d74b3518c8ba9b9eb38087378136.png
new file mode 100644
index 0000000..b2aee88
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/html/data/ee4a5843da13d74b3518c8ba9b9eb38087378136.png differ
diff --git a/reports/api-error-messages-20260906/authentication/html/index.html b/reports/api-error-messages-20260906/authentication/html/index.html
new file mode 100644
index 0000000..5ae2de3
--- /dev/null
+++ b/reports/api-error-messages-20260906/authentication/html/index.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+ Playwright Test Report
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/reports/api-error-messages-20260906/authentication/login-message/html/data/2b25574bd5f531457708c96732dfba86687cae2e.png b/reports/api-error-messages-20260906/authentication/login-message/html/data/2b25574bd5f531457708c96732dfba86687cae2e.png
new file mode 100644
index 0000000..9b54113
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/login-message/html/data/2b25574bd5f531457708c96732dfba86687cae2e.png differ
diff --git a/reports/api-error-messages-20260906/authentication/login-message/html/data/4f9a3687e54a18461593aa85754af63334217de0.png b/reports/api-error-messages-20260906/authentication/login-message/html/data/4f9a3687e54a18461593aa85754af63334217de0.png
new file mode 100644
index 0000000..d109da8
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/login-message/html/data/4f9a3687e54a18461593aa85754af63334217de0.png differ
diff --git a/reports/api-error-messages-20260906/authentication/login-message/html/index.html b/reports/api-error-messages-20260906/authentication/login-message/html/index.html
new file mode 100644
index 0000000..3807666
--- /dev/null
+++ b/reports/api-error-messages-20260906/authentication/login-message/html/index.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+ Playwright Test Report
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/reports/api-error-messages-20260906/authentication/login-message/results.json b/reports/api-error-messages-20260906/authentication/login-message/results.json
new file mode 100644
index 0000000..ffecedd
--- /dev/null
+++ b/reports/api-error-messages-20260906/authentication/login-message/results.json
@@ -0,0 +1,130 @@
+{
+ "config": {
+ "configFile": "E:\\work\\edit_3d\\.codex-tmp\\auth-login-message.config.ts",
+ "rootDir": "E:/work/edit_3d/unreal_tran/ute2e/tests",
+ "forbidOnly": false,
+ "fullyParallel": false,
+ "globalSetup": null,
+ "globalTeardown": null,
+ "globalTimeout": 0,
+ "grep": {},
+ "grepInvert": null,
+ "maxFailures": 0,
+ "metadata": {
+ "actualWorkers": 1
+ },
+ "preserveOutput": "always",
+ "reporter": [
+ [
+ "list",
+ null
+ ],
+ [
+ "html",
+ {
+ "outputFolder": "E:/work/edit_3d/unreal_tran/ute2e/reports/api-error-messages-20260906/authentication/login-message/html",
+ "open": "never"
+ }
+ ],
+ [
+ "json",
+ {
+ "outputFile": "E:/work/edit_3d/unreal_tran/ute2e/reports/api-error-messages-20260906/authentication/login-message/results.json"
+ }
+ ]
+ ],
+ "reportSlowTests": {
+ "max": 5,
+ "threshold": 300000
+ },
+ "quiet": false,
+ "projects": [
+ {
+ "outputDir": "E:/work/edit_3d/unreal_tran/ute2e/reports/api-error-messages-20260906/authentication/login-message/test-results",
+ "repeatEach": 1,
+ "retries": 0,
+ "metadata": {
+ "actualWorkers": 1
+ },
+ "id": "edge-auth-mock",
+ "name": "edge-auth-mock",
+ "testDir": "E:/work/edit_3d/unreal_tran/ute2e/tests",
+ "testIgnore": [],
+ "testMatch": [
+ "zentao-auth-859-861-864.spec.ts"
+ ],
+ "timeout": 90000
+ }
+ ],
+ "shard": null,
+ "updateSnapshots": "missing",
+ "updateSourceMethod": "patch",
+ "version": "1.55.0",
+ "workers": 1,
+ "webServer": null
+ },
+ "suites": [
+ {
+ "title": "zentao-auth-859-861-864.spec.ts",
+ "file": "zentao-auth-859-861-864.spec.ts",
+ "column": 0,
+ "line": 0,
+ "specs": [
+ {
+ "title": "当前登录接口失败只显示临时 Message,自动消失且不留下登录错误条",
+ "ok": true,
+ "tags": [],
+ "tests": [
+ {
+ "timeout": 90000,
+ "annotations": [],
+ "expectedStatus": "passed",
+ "projectId": "edge-auth-mock",
+ "projectName": "edge-auth-mock",
+ "results": [
+ {
+ "workerIndex": 0,
+ "parallelIndex": 0,
+ "status": "passed",
+ "duration": 8478,
+ "errors": [],
+ "stdout": [],
+ "stderr": [],
+ "retry": 0,
+ "startTime": "2026-09-05T17:32:36.419Z",
+ "annotations": [],
+ "attachments": [
+ {
+ "name": "success-login-api-error-message",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\login-message\\test-results\\zentao-auth-859-861-864-当前登录接口失败只显示临时-Message,自动消失且不留下登录错误条-edge-auth-mock\\attachments\\success-login-api-error-message-3f0f8df181e67a1ff135d013664efb82b9b5f8b0.png"
+ },
+ {
+ "name": "success-login-api-error-message-dismissed",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\login-message\\test-results\\zentao-auth-859-861-864-当前登录接口失败只显示临时-Message,自动消失且不留下登录错误条-edge-auth-mock\\attachments\\success-login-api-error-message-dismissed-5f52bcb97a02d086fed56c692ffe02065362ffd1.png"
+ }
+ ]
+ }
+ ],
+ "status": "expected"
+ }
+ ],
+ "id": "52adcd77a1936aed13a9-423cf20f38d8d58278a5",
+ "file": "zentao-auth-859-861-864.spec.ts",
+ "line": 160,
+ "column": 1
+ }
+ ]
+ }
+ ],
+ "errors": [],
+ "stats": {
+ "startTime": "2026-09-05T17:32:35.175Z",
+ "duration": 10788.947,
+ "expected": 1,
+ "skipped": 0,
+ "unexpected": 0,
+ "flaky": 0
+ }
+}
\ No newline at end of file
diff --git a/reports/api-error-messages-20260906/authentication/login-message/run.log b/reports/api-error-messages-20260906/authentication/login-message/run.log
new file mode 100644
index 0000000..e6af136
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/login-message/run.log differ
diff --git a/reports/api-error-messages-20260906/authentication/login-message/screenshots/login-api-error-message-dismissed.png b/reports/api-error-messages-20260906/authentication/login-message/screenshots/login-api-error-message-dismissed.png
new file mode 100644
index 0000000..d109da8
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/login-message/screenshots/login-api-error-message-dismissed.png differ
diff --git a/reports/api-error-messages-20260906/authentication/login-message/screenshots/login-api-error-message.png b/reports/api-error-messages-20260906/authentication/login-message/screenshots/login-api-error-message.png
new file mode 100644
index 0000000..9b54113
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/login-message/screenshots/login-api-error-message.png differ
diff --git a/reports/api-error-messages-20260906/authentication/login-message/test-results/zentao-auth-859-861-864-当前登录接口失败只显示临时-Message,自动消失且不留下登录错误条-edge-auth-mock/attachments/success-login-api-error-message-3f0f8df181e67a1ff135d013664efb82b9b5f8b0.png b/reports/api-error-messages-20260906/authentication/login-message/test-results/zentao-auth-859-861-864-当前登录接口失败只显示临时-Message,自动消失且不留下登录错误条-edge-auth-mock/attachments/success-login-api-error-message-3f0f8df181e67a1ff135d013664efb82b9b5f8b0.png
new file mode 100644
index 0000000..9b54113
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/login-message/test-results/zentao-auth-859-861-864-当前登录接口失败只显示临时-Message,自动消失且不留下登录错误条-edge-auth-mock/attachments/success-login-api-error-message-3f0f8df181e67a1ff135d013664efb82b9b5f8b0.png differ
diff --git a/reports/api-error-messages-20260906/authentication/login-message/test-results/zentao-auth-859-861-864-当前登录接口失败只显示临时-Message,自动消失且不留下登录错误条-edge-auth-mock/attachments/success-login-api-error-message-dismissed-5f52bcb97a02d086fed56c692ffe02065362ffd1.png b/reports/api-error-messages-20260906/authentication/login-message/test-results/zentao-auth-859-861-864-当前登录接口失败只显示临时-Message,自动消失且不留下登录错误条-edge-auth-mock/attachments/success-login-api-error-message-dismissed-5f52bcb97a02d086fed56c692ffe02065362ffd1.png
new file mode 100644
index 0000000..d109da8
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/login-message/test-results/zentao-auth-859-861-864-当前登录接口失败只显示临时-Message,自动消失且不留下登录错误条-edge-auth-mock/attachments/success-login-api-error-message-dismissed-5f52bcb97a02d086fed56c692ffe02065362ffd1.png differ
diff --git a/reports/api-error-messages-20260906/authentication/results.json b/reports/api-error-messages-20260906/authentication/results.json
new file mode 100644
index 0000000..705fe15
--- /dev/null
+++ b/reports/api-error-messages-20260906/authentication/results.json
@@ -0,0 +1,307 @@
+{
+ "config": {
+ "configFile": "E:\\work\\edit_3d\\.codex-tmp\\auth-message-regression.config.ts",
+ "rootDir": "E:/work/edit_3d/unreal_tran/ute2e/tests",
+ "forbidOnly": false,
+ "fullyParallel": false,
+ "globalSetup": null,
+ "globalTeardown": null,
+ "globalTimeout": 0,
+ "grep": {},
+ "grepInvert": null,
+ "maxFailures": 0,
+ "metadata": {
+ "actualWorkers": 1
+ },
+ "preserveOutput": "always",
+ "reporter": [
+ [
+ "list",
+ null
+ ],
+ [
+ "html",
+ {
+ "outputFolder": "E:/work/edit_3d/unreal_tran/ute2e/reports/api-error-messages-20260906/authentication/html",
+ "open": "never"
+ }
+ ],
+ [
+ "json",
+ {
+ "outputFile": "E:/work/edit_3d/unreal_tran/ute2e/reports/api-error-messages-20260906/authentication/results.json"
+ }
+ ]
+ ],
+ "reportSlowTests": {
+ "max": 5,
+ "threshold": 300000
+ },
+ "quiet": false,
+ "projects": [
+ {
+ "outputDir": "E:/work/edit_3d/unreal_tran/ute2e/reports/api-error-messages-20260906/authentication/test-results",
+ "repeatEach": 1,
+ "retries": 0,
+ "metadata": {
+ "actualWorkers": 1
+ },
+ "id": "edge-auth-mock",
+ "name": "edge-auth-mock",
+ "testDir": "E:/work/edit_3d/unreal_tran/ute2e/tests",
+ "testIgnore": [],
+ "testMatch": [
+ "zentao-auth-859-861-864.spec.ts"
+ ],
+ "timeout": 90000
+ }
+ ],
+ "shard": null,
+ "updateSnapshots": "missing",
+ "updateSourceMethod": "patch",
+ "version": "1.55.0",
+ "workers": 1,
+ "webServer": null
+ },
+ "suites": [
+ {
+ "title": "zentao-auth-859-861-864.spec.ts",
+ "file": "zentao-auth-859-861-864.spec.ts",
+ "column": 0,
+ "line": 0,
+ "specs": [
+ {
+ "title": "859:部门没有可登录教员时账号选择器明确禁用且不可展开",
+ "ok": true,
+ "tags": [],
+ "tests": [
+ {
+ "timeout": 90000,
+ "annotations": [],
+ "expectedStatus": "passed",
+ "projectId": "edge-auth-mock",
+ "projectName": "edge-auth-mock",
+ "results": [
+ {
+ "workerIndex": 0,
+ "parallelIndex": 0,
+ "status": "passed",
+ "duration": 3850,
+ "errors": [],
+ "stdout": [],
+ "stderr": [],
+ "retry": 0,
+ "startTime": "2026-09-05T17:30:12.947Z",
+ "annotations": [],
+ "attachments": [
+ {
+ "name": "success-859-empty-teacher-account-disabled",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\test-results\\zentao-auth-859-861-864-859:部门没有可登录教员时账号选择器明确禁用且不可展开-edge-auth-mock\\attachments\\success-859-empty-teacher-account-disabled-2f99562734701ae457b390f2ea2477898d51907c.png"
+ }
+ ]
+ }
+ ],
+ "status": "expected"
+ }
+ ],
+ "id": "52adcd77a1936aed13a9-d7d9043f0521ae86a405",
+ "file": "zentao-auth-859-861-864.spec.ts",
+ "line": 94,
+ "column": 1
+ },
+ {
+ "title": "861:首次改密两次输入不一致时立即显示字段提示且不提交",
+ "ok": false,
+ "tags": [],
+ "tests": [
+ {
+ "timeout": 90000,
+ "annotations": [],
+ "expectedStatus": "passed",
+ "projectId": "edge-auth-mock",
+ "projectName": "edge-auth-mock",
+ "results": [
+ {
+ "workerIndex": 0,
+ "parallelIndex": 0,
+ "status": "failed",
+ "duration": 16889,
+ "error": {
+ "message": "TimeoutError: locator.fill: Timeout 15000ms exceeded.\nCall log:\n\u001b[2m - waiting for locator('.password-card input[autocomplete=\"new-password\"]').first()\u001b[22m\n",
+ "stack": "TimeoutError: locator.fill: Timeout 15000ms exceeded.\nCall log:\n\u001b[2m - waiting for locator('.password-card input[autocomplete=\"new-password\"]').first()\u001b[22m\n\n at E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts:114:23",
+ "location": {
+ "file": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts",
+ "column": 23,
+ "line": 114
+ },
+ "snippet": " 112 | await page.goto('/change-password')\n 113 | const inputs = page.locator('.password-card input[autocomplete=\"new-password\"]')\n> 114 | await inputs.nth(0).fill('Strong#Password8')\n | ^\n 115 | await inputs.nth(1).fill('Different#Pass9')\n 116 |\n 117 | await expect(page.getByText('两次输入的新密码不一致', { exact: true })).toBeVisible()"
+ },
+ "errors": [
+ {
+ "location": {
+ "file": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts",
+ "column": 23,
+ "line": 114
+ },
+ "message": "TimeoutError: locator.fill: Timeout 15000ms exceeded.\nCall log:\n\u001b[2m - waiting for locator('.password-card input[autocomplete=\"new-password\"]').first()\u001b[22m\n\n\n 112 | await page.goto('/change-password')\n 113 | const inputs = page.locator('.password-card input[autocomplete=\"new-password\"]')\n> 114 | await inputs.nth(0).fill('Strong#Password8')\n | ^\n 115 | await inputs.nth(1).fill('Different#Pass9')\n 116 |\n 117 | await expect(page.getByText('两次输入的新密码不一致', { exact: true })).toBeVisible()\n at E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts:114:23"
+ }
+ ],
+ "stdout": [],
+ "stderr": [],
+ "retry": 0,
+ "startTime": "2026-09-05T17:30:17.307Z",
+ "annotations": [],
+ "attachments": [
+ {
+ "name": "screenshot",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\test-results\\zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock\\test-failed-1.png"
+ },
+ {
+ "name": "failure-screenshot",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\test-results\\zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock\\attachments\\failure-screenshot-6fd2d23a12532f4a17e77e33991ddeb1f050286e.png"
+ },
+ {
+ "name": "error-context",
+ "contentType": "text/markdown",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\test-results\\zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock\\error-context.md"
+ }
+ ],
+ "errorLocation": {
+ "file": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts",
+ "column": 23,
+ "line": 114
+ }
+ }
+ ],
+ "status": "unexpected"
+ }
+ ],
+ "id": "52adcd77a1936aed13a9-025f02a9349d3fb78768",
+ "file": "zentao-auth-859-861-864.spec.ts",
+ "line": 109,
+ "column": 1
+ },
+ {
+ "title": "864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌",
+ "ok": false,
+ "tags": [],
+ "tests": [
+ {
+ "timeout": 90000,
+ "annotations": [],
+ "expectedStatus": "passed",
+ "projectId": "edge-auth-mock",
+ "projectName": "edge-auth-mock",
+ "results": [
+ {
+ "workerIndex": 1,
+ "parallelIndex": 0,
+ "status": "failed",
+ "duration": 16450,
+ "error": {
+ "message": "TimeoutError: locator.fill: Timeout 15000ms exceeded.\nCall log:\n\u001b[2m - waiting for locator('.password-card input[autocomplete=\"new-password\"]').first()\u001b[22m\n",
+ "stack": "TimeoutError: locator.fill: Timeout 15000ms exceeded.\nCall log:\n\u001b[2m - waiting for locator('.password-card input[autocomplete=\"new-password\"]').first()\u001b[22m\n\n at E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts:128:23",
+ "location": {
+ "file": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts",
+ "column": 23,
+ "line": 128
+ },
+ "snippet": " 126 | await page.goto('/change-password')\n 127 | const inputs = page.locator('.password-card input[autocomplete=\"new-password\"]')\n> 128 | await inputs.nth(0).fill('Strong#Password8')\n | ^\n 129 | await inputs.nth(1).fill('Strong#Password8')\n 130 | await page.getByRole('button', { name: '确认修改并登录' }).click()\n 131 |"
+ },
+ "errors": [
+ {
+ "location": {
+ "file": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts",
+ "column": 23,
+ "line": 128
+ },
+ "message": "TimeoutError: locator.fill: Timeout 15000ms exceeded.\nCall log:\n\u001b[2m - waiting for locator('.password-card input[autocomplete=\"new-password\"]').first()\u001b[22m\n\n\n 126 | await page.goto('/change-password')\n 127 | const inputs = page.locator('.password-card input[autocomplete=\"new-password\"]')\n> 128 | await inputs.nth(0).fill('Strong#Password8')\n | ^\n 129 | await inputs.nth(1).fill('Strong#Password8')\n 130 | await page.getByRole('button', { name: '确认修改并登录' }).click()\n 131 |\n at E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts:128:23"
+ }
+ ],
+ "stdout": [],
+ "stderr": [],
+ "retry": 0,
+ "startTime": "2026-09-05T17:30:35.470Z",
+ "annotations": [],
+ "attachments": [
+ {
+ "name": "screenshot",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\test-results\\zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock\\test-failed-1.png"
+ },
+ {
+ "name": "failure-screenshot",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\test-results\\zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock\\attachments\\failure-screenshot-2a08b4d1dc5dcad4acf34c6b179a1b6ae4b903de.png"
+ },
+ {
+ "name": "error-context",
+ "contentType": "text/markdown",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\test-results\\zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock\\error-context.md"
+ }
+ ],
+ "errorLocation": {
+ "file": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts",
+ "column": 23,
+ "line": 128
+ }
+ }
+ ],
+ "status": "unexpected"
+ }
+ ],
+ "id": "52adcd77a1936aed13a9-e99934e8cad148ac65c9",
+ "file": "zentao-auth-859-861-864.spec.ts",
+ "line": 123,
+ "column": 1
+ },
+ {
+ "title": "普通会话过期只跳转登录,不展示后端错误详情或停用账号提示",
+ "ok": true,
+ "tags": [],
+ "tests": [
+ {
+ "timeout": 90000,
+ "annotations": [],
+ "expectedStatus": "passed",
+ "projectId": "edge-auth-mock",
+ "projectName": "edge-auth-mock",
+ "results": [
+ {
+ "workerIndex": 2,
+ "parallelIndex": 0,
+ "status": "passed",
+ "duration": 2493,
+ "errors": [],
+ "stdout": [],
+ "stderr": [],
+ "retry": 0,
+ "startTime": "2026-09-05T17:30:53.683Z",
+ "annotations": [],
+ "attachments": []
+ }
+ ],
+ "status": "expected"
+ }
+ ],
+ "id": "52adcd77a1936aed13a9-cdec245d1f279f3a22ad",
+ "file": "zentao-auth-859-861-864.spec.ts",
+ "line": 143,
+ "column": 1
+ }
+ ]
+ }
+ ],
+ "errors": [],
+ "stats": {
+ "startTime": "2026-09-05T17:30:11.840Z",
+ "duration": 45375.929000000004,
+ "expected": 2,
+ "skipped": 0,
+ "unexpected": 2,
+ "flaky": 0
+ }
+}
\ No newline at end of file
diff --git a/reports/api-error-messages-20260906/authentication/run.log b/reports/api-error-messages-20260906/authentication/run.log
new file mode 100644
index 0000000..fc1912a
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/run.log differ
diff --git a/reports/api-error-messages-20260906/authentication/screenshots/859-passed.png b/reports/api-error-messages-20260906/authentication/screenshots/859-passed.png
new file mode 100644
index 0000000..723a12d
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/screenshots/859-passed.png differ
diff --git a/reports/api-error-messages-20260906/authentication/screenshots/861-failed.png b/reports/api-error-messages-20260906/authentication/screenshots/861-failed.png
new file mode 100644
index 0000000..0d864ce
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/screenshots/861-failed.png differ
diff --git a/reports/api-error-messages-20260906/authentication/screenshots/864-failed.png b/reports/api-error-messages-20260906/authentication/screenshots/864-failed.png
new file mode 100644
index 0000000..5bc0d6b
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/screenshots/864-failed.png differ
diff --git a/reports/api-error-messages-20260906/authentication/summary.json b/reports/api-error-messages-20260906/authentication/summary.json
new file mode 100644
index 0000000..6273127
--- /dev/null
+++ b/reports/api-error-messages-20260906/authentication/summary.json
@@ -0,0 +1,66 @@
+{
+ "title": "认证提示附加回归",
+ "status": "PASS_WITH_LEGACY_BLOCKERS",
+ "result": "共实际运行 5 项:3 项通过;2 项旧强制改密用例受当前路由契约阻断,未验证到原断言。主报告 9 项结果单独统计。",
+ "screenshotPathBase": "api-error-messages-20260906",
+ "environment": "Edge headless,http://127.0.0.1:6180,认证 API 全部由 Playwright 拦截模拟;trace/video 关闭。",
+ "counts": { "executed": 5, "passed": 3, "legacyBlocked": 2, "skipped": 0 },
+ "runs": [
+ {
+ "command": "pnpm exec playwright test --config E:\\work\\edit_3d\\.codex-tmp\\auth-message-regression.config.ts",
+ "result": "首轮运行当时文件中的 4 项旧回归,2 passed / 2 failed(均为输入定位超时),用时 45.4 秒;原始结果保留。",
+ "json": "authentication/results.json",
+ "html": "authentication/html/index.html",
+ "log": "authentication/run.log"
+ },
+ {
+ "command": "pnpm exec playwright test --config E:\\work\\edit_3d\\.codex-tmp\\auth-login-message.config.ts",
+ "result": "新增当前登录 API 错误回归独立执行,1 passed,用时 10.8 秒。",
+ "json": "authentication/login-message/results.json",
+ "html": "authentication/login-message/html/index.html",
+ "log": "authentication/login-message/run.log"
+ }
+ ],
+ "checks": [
+ {
+ "title": "859:无可登录教员时账号选择器禁用",
+ "status": "PASS",
+ "result": "保留业务空态,选择器不可展开。",
+ "screenshots": [{ "title": "无教员业务空态", "path": "authentication/screenshots/859-passed.png" }]
+ },
+ {
+ "title": "861:旧强制改密不一致校验",
+ "status": "BLOCKED",
+ "rawStatus": "failed",
+ "result": "当前 /change-password 对已认证用户重定向 /dashboard;旧 fixture 只有强制改密标记而无工作台权限,后续导航取消,停留在启动页,未进入改密表单。该旧用例不适用于当前路由契约,本次未改路由、未跳过或记为通过。",
+ "screenshots": [{ "title": "旧改密入口阻断", "path": "authentication/screenshots/861-failed.png" }]
+ },
+ {
+ "title": "864:旧强制改密停用账号提示",
+ "status": "BLOCKED",
+ "rawStatus": "failed",
+ "result": "与 861 相同的既有路由契约阻断,未能提交初始改密接口,也未验证一次性登录提示。原始失败证据保留。",
+ "screenshots": [{ "title": "旧停用账号流程阻断", "path": "authentication/screenshots/864-failed.png" }]
+ },
+ {
+ "title": "普通会话过期不泄露后端内部详情",
+ "status": "PASS",
+ "result": "跳转登录页,登录错误栏为空,不显示内部会话或刷新令牌详情,仅发起一次刷新请求。",
+ "screenshots": []
+ },
+ {
+ "title": "当前管理员登录 API 401 错误使用自动消失的 Message",
+ "status": "PASS",
+ "result": "实际点击登录;mock POST /auth/login 返回 401。只出现一条 .el-message--error,#login-error 始终为空;未点击关闭,Message 自动消失。登录请求 1 次,刷新请求 0 次。",
+ "screenshots": [
+ { "title": "登录失败 Message", "path": "authentication/login-message/screenshots/login-api-error-message.png" },
+ { "title": "Message 自动消失后", "path": "authentication/login-message/screenshots/login-api-error-message-dismissed.png" }
+ ]
+ }
+ ],
+ "limitations": [
+ "仅模拟认证接口与浏览器交互,不使用真实账号,不验证后端认证规则或真实密码修改。",
+ "未运行依赖真实账号的 auth.spec.ts 全量;仅更新其中接口错误提示的断言选择器。",
+ "两条旧强制改密用例没有通过,不能作为当前改密功能验证证据;既有路由策略不在本轮提示展示调整范围。"
+ ]
+}
diff --git a/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-859:部门没有可登录教员时账号选择器明确禁用且不可展开-edge-auth-mock/attachments/success-859-empty-teacher-account-disabled-2f99562734701ae457b390f2ea2477898d51907c.png b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-859:部门没有可登录教员时账号选择器明确禁用且不可展开-edge-auth-mock/attachments/success-859-empty-teacher-account-disabled-2f99562734701ae457b390f2ea2477898d51907c.png
new file mode 100644
index 0000000..723a12d
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-859:部门没有可登录教员时账号选择器明确禁用且不可展开-edge-auth-mock/attachments/success-859-empty-teacher-account-disabled-2f99562734701ae457b390f2ea2477898d51907c.png differ
diff --git a/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/attachments/failure-screenshot-6fd2d23a12532f4a17e77e33991ddeb1f050286e.png b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/attachments/failure-screenshot-6fd2d23a12532f4a17e77e33991ddeb1f050286e.png
new file mode 100644
index 0000000..b2aee88
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/attachments/failure-screenshot-6fd2d23a12532f4a17e77e33991ddeb1f050286e.png differ
diff --git a/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/error-context.md b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/error-context.md
new file mode 100644
index 0000000..5ad12ed
--- /dev/null
+++ b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/error-context.md
@@ -0,0 +1,10 @@
+# Page snapshot
+
+```yaml
+- status [ref=e3]:
+ - generic [ref=e4]:
+ - strong [ref=e7]: 装备数字车间
+ - generic [ref=e10]:
+ - generic [ref=e11]: 正在进入系统
+ - paragraph [ref=e12]: 正在验证登录状态并加载工作台,请稍候…
+```
\ No newline at end of file
diff --git a/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/test-failed-1.png b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/test-failed-1.png
new file mode 100644
index 0000000..0d864ce
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/test-failed-1.png differ
diff --git a/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/attachments/failure-screenshot-2a08b4d1dc5dcad4acf34c6b179a1b6ae4b903de.png b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/attachments/failure-screenshot-2a08b4d1dc5dcad4acf34c6b179a1b6ae4b903de.png
new file mode 100644
index 0000000..4f0f49e
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/attachments/failure-screenshot-2a08b4d1dc5dcad4acf34c6b179a1b6ae4b903de.png differ
diff --git a/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/error-context.md b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/error-context.md
new file mode 100644
index 0000000..5ad12ed
--- /dev/null
+++ b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/error-context.md
@@ -0,0 +1,10 @@
+# Page snapshot
+
+```yaml
+- status [ref=e3]:
+ - generic [ref=e4]:
+ - strong [ref=e7]: 装备数字车间
+ - generic [ref=e10]:
+ - generic [ref=e11]: 正在进入系统
+ - paragraph [ref=e12]: 正在验证登录状态并加载工作台,请稍候…
+```
\ No newline at end of file
diff --git a/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/test-failed-1.png b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/test-failed-1.png
new file mode 100644
index 0000000..5bc0d6b
Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/test-failed-1.png differ
diff --git a/reports/api-error-messages-20260906/evidence.json b/reports/api-error-messages-20260906/evidence.json
new file mode 100644
index 0000000..04d9fd0
--- /dev/null
+++ b/reports/api-error-messages-20260906/evidence.json
@@ -0,0 +1,625 @@
+{
+ "title": "接口错误 Message 回归报告",
+ "status": "PASS_WITH_LEGACY_BLOCKERS",
+ "generatedAt": "2026-09-05T17:36:20.720Z",
+ "startedAt": "2026-09-05T17:32:19.650Z",
+ "durationMs": 84090.698,
+ "passed": 9,
+ "total": 9,
+ "screenshotCount": 17,
+ "requestCount": 58,
+ "pageErrorCount": 0,
+ "validation": {
+ "commands": [
+ {
+ "command": "pnpm run build",
+ "scope": "unreal_tran_web",
+ "status": "PASS",
+ "details": "最终 vue-tsc、Vite 与 legacy iframe public/dist 哈希校验通过;仍有既有 chunk 体积提示。",
+ "log": "validation/build.log"
+ },
+ {
+ "command": "node --test tools/scene-cover-api.test.mjs tools/guide-error-message.test.mjs",
+ "scope": "ute2e",
+ "status": "PASS",
+ "details": "20/20 通过,包含保存失败只提示一次、阻断发布与离开,以及指导书/场景错误提示回归。",
+ "log": "validation/runtime-tests.log"
+ },
+ {
+ "command": "pnpm exec tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --strict --types 'node,@playwright/test' tests/api-error-messages.spec.ts api-error-messages.config.ts",
+ "scope": "ute2e",
+ "status": "PASS",
+ "details": "本轮新增浏览器测试与配置类型检查通过。"
+ },
+ {
+ "command": "git diff --check",
+ "scope": "unreal_tran_web、ute2e",
+ "status": "PASS",
+ "details": "主任务检查通过;报告生成后再次核验本轮文件。"
+ }
+ ]
+ },
+ "authentication": {
+ "title": "认证提示附加回归",
+ "status": "PASS_WITH_LEGACY_BLOCKERS",
+ "result": "共实际运行 5 项:3 项通过;2 项旧强制改密用例受当前路由契约阻断,未验证到原断言。主报告 9 项结果单独统计。",
+ "screenshotPathBase": "api-error-messages-20260906",
+ "environment": "Edge headless,http://127.0.0.1:6180,认证 API 全部由 Playwright 拦截模拟;trace/video 关闭。",
+ "counts": {
+ "executed": 5,
+ "passed": 3,
+ "legacyBlocked": 2,
+ "skipped": 0
+ },
+ "runs": [
+ {
+ "command": "pnpm exec playwright test --config E:\\work\\edit_3d\\.codex-tmp\\auth-message-regression.config.ts",
+ "result": "首轮运行当时文件中的 4 项旧回归,2 passed / 2 failed(均为输入定位超时),用时 45.4 秒;原始结果保留。",
+ "json": "authentication/results.json",
+ "html": "authentication/html/index.html",
+ "log": "authentication/run.log"
+ },
+ {
+ "command": "pnpm exec playwright test --config E:\\work\\edit_3d\\.codex-tmp\\auth-login-message.config.ts",
+ "result": "新增当前登录 API 错误回归独立执行,1 passed,用时 10.8 秒。",
+ "json": "authentication/login-message/results.json",
+ "html": "authentication/login-message/html/index.html",
+ "log": "authentication/login-message/run.log"
+ }
+ ],
+ "checks": [
+ {
+ "title": "859:无可登录教员时账号选择器禁用",
+ "status": "PASS",
+ "result": "保留业务空态,选择器不可展开。",
+ "screenshots": [
+ {
+ "title": "无教员业务空态",
+ "path": "authentication/screenshots/859-passed.png"
+ }
+ ]
+ },
+ {
+ "title": "861:旧强制改密不一致校验",
+ "status": "BLOCKED",
+ "rawStatus": "failed",
+ "result": "当前 /change-password 对已认证用户重定向 /dashboard;旧 fixture 只有强制改密标记而无工作台权限,后续导航取消,停留在启动页,未进入改密表单。该旧用例不适用于当前路由契约,本次未改路由、未跳过或记为通过。",
+ "screenshots": [
+ {
+ "title": "旧改密入口阻断",
+ "path": "authentication/screenshots/861-failed.png"
+ }
+ ]
+ },
+ {
+ "title": "864:旧强制改密停用账号提示",
+ "status": "BLOCKED",
+ "rawStatus": "failed",
+ "result": "与 861 相同的既有路由契约阻断,未能提交初始改密接口,也未验证一次性登录提示。原始失败证据保留。",
+ "screenshots": [
+ {
+ "title": "旧停用账号流程阻断",
+ "path": "authentication/screenshots/864-failed.png"
+ }
+ ]
+ },
+ {
+ "title": "普通会话过期不泄露后端内部详情",
+ "status": "PASS",
+ "result": "跳转登录页,登录错误栏为空,不显示内部会话或刷新令牌详情,仅发起一次刷新请求。",
+ "screenshots": []
+ },
+ {
+ "title": "当前管理员登录 API 401 错误使用自动消失的 Message",
+ "status": "PASS",
+ "result": "实际点击登录;mock POST /auth/login 返回 401。只出现一条 .el-message--error,#login-error 始终为空;未点击关闭,Message 自动消失。登录请求 1 次,刷新请求 0 次。",
+ "screenshots": [
+ {
+ "title": "登录失败 Message",
+ "path": "authentication/login-message/screenshots/login-api-error-message.png"
+ },
+ {
+ "title": "Message 自动消失后",
+ "path": "authentication/login-message/screenshots/login-api-error-message-dismissed.png"
+ }
+ ]
+ }
+ ],
+ "limitations": [
+ "仅模拟认证接口与浏览器交互,不使用真实账号,不验证后端认证规则或真实密码修改。",
+ "未运行依赖真实账号的 auth.spec.ts 全量;仅更新其中接口错误提示的断言选择器。",
+ "两条旧强制改密用例没有通过,不能作为当前改密功能验证证据;既有路由策略不在本轮提示展示调整范围。"
+ ]
+ },
+ "stages": [
+ {
+ "title": "资料",
+ "text": "按本轮“接口错误使用 Message,不保留常驻错误横条”的要求检查。沿用已确认的教学业务边界:离线识别回放、观察模式、步骤反馈和表单校验仍需可见。"
+ },
+ {
+ "title": "原型",
+ "text": "错误发生时顶部短暂提示;可以手动关闭,约 4.5 秒自动消失。同一错误合并展示;失败后仍可重新加载,已成功取得的数据不因后续刷新失败消失。"
+ },
+ {
+ "title": "设计",
+ "text": "共享 Message 处理负责统一展示与缓存页面生命周期;请求层保留业务错误码、HTTP 状态和 requestId,静默请求不会自行弹窗。未取得数据与空结果保持区分。"
+ },
+ {
+ "title": "开发",
+ "text": "代表性覆盖内容制作、系统用户与教学实施,包括训练编辑器加载失败的重试入口。接口异常详情仅在 Message 中展示;表单必填错误、项目编码说明和离线运行模式继续保留。"
+ },
+ {
+ "title": "测试",
+ "text": "使用真实 Vue / Element Plus 页面、Edge 无头浏览器和浏览器级 API mock。覆盖本机与局域网入口。身份及接口数据均为合成测试数据;不登录真实账号、不写业务库,不将本轮测试称为真实服务端或设备联调。"
+ }
+ ],
+ "checks": [
+ {
+ "title": "内容列表:错误自动消失、重试恢复、再次失败保留旧数据",
+ "status": "PASS",
+ "durationMs": 12754,
+ "screenshots": [
+ {
+ "title": "01-content-error-message",
+ "path": "screenshots/01-content-error-message.png"
+ },
+ {
+ "title": "02-content-old-data-retained",
+ "path": "screenshots/02-content-old-data-retained.png"
+ }
+ ],
+ "isolation": {
+ "requests": [
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/system-config/public"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/auth/me"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/menus/navigation"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/content/projects/summary"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/content/projects"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/content/projects"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/content/projects/summary"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/content/projects"
+ }
+ ],
+ "unexpected": [],
+ "writes": [],
+ "pageErrors": []
+ }
+ },
+ {
+ "title": "系统用户:并行同错合并一条,重复搜索可再次提示",
+ "status": "PASS",
+ "durationMs": 12705,
+ "screenshots": [
+ {
+ "title": "03-users-grouped-message",
+ "path": "screenshots/03-users-grouped-message.png"
+ },
+ {
+ "title": "04-users-retry-without-banner",
+ "path": "screenshots/04-users-retry-without-banner.png"
+ }
+ ],
+ "isolation": {
+ "requests": [
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/system-config/public"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/auth/me"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/menus/navigation"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/roles/all"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/departments/tree"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/users"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/users/stats"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/users"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/users/stats"
+ }
+ ],
+ "unexpected": [],
+ "writes": [],
+ "pageErrors": []
+ }
+ },
+ {
+ "title": "训练编辑器:加载失败提示消失后保留重新加载按钮",
+ "status": "PASS",
+ "durationMs": 6385,
+ "screenshots": [
+ {
+ "title": "05-legacy-training-message",
+ "path": "screenshots/05-legacy-training-message.png"
+ },
+ {
+ "title": "06-legacy-training-retry",
+ "path": "screenshots/06-legacy-training-retry.png"
+ }
+ ],
+ "isolation": {
+ "requests": [
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/system-config/public"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/auth/me"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/menus/navigation"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/content/projects/message-training-901"
+ }
+ ],
+ "unexpected": [],
+ "writes": [],
+ "pageErrors": []
+ }
+ },
+ {
+ "title": "实装预览:任务上下文错误用 Message,离线与教员模式持续可见",
+ "status": "PASS",
+ "durationMs": 6450,
+ "screenshots": [
+ {
+ "title": "07-physical-context-message",
+ "path": "screenshots/07-physical-context-message.png"
+ },
+ {
+ "title": "08-physical-mode-preserved",
+ "path": "screenshots/08-physical-mode-preserved.png"
+ }
+ ],
+ "isolation": {
+ "requests": [
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/system-config/public"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/auth/me"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/menus/navigation"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/teaching/assignments/mock-physical"
+ }
+ ],
+ "unexpected": [],
+ "writes": [],
+ "pageErrors": []
+ }
+ },
+ {
+ "title": "新建训练:业务说明与必填校验保留,不发起创建请求",
+ "status": "PASS",
+ "durationMs": 2485,
+ "screenshots": [
+ {
+ "title": "09-create-validation-preserved",
+ "path": "screenshots/09-create-validation-preserved.png"
+ }
+ ],
+ "isolation": {
+ "requests": [
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/system-config/public"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/auth/me"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/menus/navigation"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/content/projects/summary"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/content/projects"
+ }
+ ],
+ "unexpected": [],
+ "writes": [],
+ "pageErrors": []
+ }
+ },
+ {
+ "title": "缓存列表:失活时晚到错误不弹出,返回不重放旧错误",
+ "status": "PASS",
+ "durationMs": 1947,
+ "screenshots": [
+ {
+ "title": "10-inactive-page-no-message",
+ "path": "screenshots/10-inactive-page-no-message.png"
+ },
+ {
+ "title": "11-reactivated-no-stale-message",
+ "path": "screenshots/11-reactivated-no-stale-message.png"
+ }
+ ],
+ "isolation": {
+ "requests": [
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/system-config/public"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/auth/me"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/menus/navigation"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/content/projects/summary"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/content/projects"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/content/projects/summary"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/content/projects"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/roles/all"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/departments/tree"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/users"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/users/stats"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/content/projects/summary"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/content/projects"
+ }
+ ],
+ "unexpected": [],
+ "writes": [],
+ "pageErrors": []
+ }
+ },
+ {
+ "title": "HTTP 合同:真实 Axios 超时中文化,403/409 元数据保留,静默请求不自动弹窗",
+ "status": "PASS",
+ "durationMs": 1942,
+ "screenshots": [
+ {
+ "title": "12-http-silent-errors",
+ "path": "screenshots/12-http-silent-errors.png"
+ },
+ {
+ "title": "15-business-message-keeps-original-text",
+ "path": "screenshots/15-business-message-keeps-original-text.png"
+ }
+ ],
+ "isolation": {
+ "requests": [
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/system-config/public"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/auth/me"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/menus/navigation"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/content/projects/summary"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/content/projects"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/__message_e2e/timeout"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/__message_e2e/forbidden"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/__message_e2e/conflict"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/__message_e2e/timeout"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/__message_e2e/business-text"
+ }
+ ],
+ "unexpected": [],
+ "writes": [],
+ "pageErrors": []
+ },
+ "http": [
+ {
+ "endpoint": "timeout",
+ "message": "请求超时,请稍后重试",
+ "code": "NETWORK_ERROR",
+ "status": null,
+ "requestId": ""
+ },
+ {
+ "endpoint": "forbidden",
+ "message": "无权访问测试数据",
+ "code": "ACCESS_DENIED",
+ "status": 403,
+ "requestId": "mock-403"
+ },
+ {
+ "endpoint": "conflict",
+ "message": "版本已变化,请重新加载",
+ "code": "VERSION_CONFLICT",
+ "status": 409,
+ "requestId": "mock-409"
+ }
+ ],
+ "httpBoundaries": {
+ "cancellation": {
+ "name": "CanceledError",
+ "code": "ERR_CANCELED"
+ },
+ "businessError": {
+ "message": "Network Error",
+ "code": "ASSET_VALIDATION",
+ "requestId": "mock-business",
+ "status": 400
+ }
+ }
+ },
+ {
+ "title": "局域网教学列表:既有 ElMessage.error 使用全局关闭和自动消失配置",
+ "status": "PASS",
+ "durationMs": 6935,
+ "screenshots": [
+ {
+ "title": "13-lan-teaching-message",
+ "path": "screenshots/13-lan-teaching-message.png"
+ },
+ {
+ "title": "14-lan-teaching-after-dismiss",
+ "path": "screenshots/14-lan-teaching-after-dismiss.png"
+ }
+ ],
+ "isolation": {
+ "requests": [
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/system-config/public"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/auth/me"
+ },
+ {
+ "method": "GET",
+ "path": "/api/auth/v1/menus/navigation"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/teaching/assignments"
+ },
+ {
+ "method": "GET",
+ "path": "/api/tran/v1/teaching/assignments/summary"
+ }
+ ],
+ "unexpected": [],
+ "writes": [],
+ "pageErrors": []
+ }
+ },
+ {
+ "title": "模型桥接 DOM 合同:初次错误与晚写详情转为重试说明,ready 恢复交互",
+ "status": "PASS",
+ "durationMs": 791,
+ "screenshots": [
+ {
+ "title": "16-model-bridge-neutral-retry",
+ "path": "screenshots/16-model-bridge-neutral-retry.png"
+ },
+ {
+ "title": "17-model-bridge-ready",
+ "path": "screenshots/17-model-bridge-ready.png"
+ }
+ ],
+ "isolation": {
+ "requests": [],
+ "unexpected": [],
+ "writes": [],
+ "pageErrors": []
+ }
+ }
+ ],
+ "limitations": [
+ "这是接口失败展示的聚焦浏览器回归,未逐一遍历所有业务页面。",
+ "页面渲染、Element Plus Message、缓存路由和 Axios 超时均运行于真实浏览器;服务端响应由测试模拟。",
+ "模型 iframe 项为真实宿主桥接的 DOM 合同测试,使用空画布夹具,不代表完整 GLB 模型加载测试。",
+ "保留训练业务状态、表单校验与重试入口。未扩大教学流程、权限或接口合同。"
+ ],
+ "reproduction": "pnpm exec playwright test -c api-error-messages.config.ts"
+}
diff --git a/reports/api-error-messages-20260906/index.html b/reports/api-error-messages-20260906/index.html
new file mode 100644
index 0000000..9e20726
--- /dev/null
+++ b/reports/api-error-messages-20260906/index.html
@@ -0,0 +1,5 @@
+
+接口错误 Message 回归报告
+接口错误 Message 回归报告
资料 → 原型 → 设计 → 开发 → 测试
主回归:代表性接口错误按统一 Message 展示;自动消失、重试与业务状态保留已通过浏览器验证。
认证附加:3 项通过,2 项旧改密入口受当前路由契约阻断;原始失败保留,未计为通过。
主回归 9 / 9 通过主回归 17 张截图58 次模拟 API 请求0 个未捕获页面异常
+审查与实施
资料按本轮“接口错误使用 Message,不保留常驻错误横条”的要求检查。沿用已确认的教学业务边界:离线识别回放、观察模式、步骤反馈和表单校验仍需可见。
原型错误发生时顶部短暂提示;可以手动关闭,约 4.5 秒自动消失。同一错误合并展示;失败后仍可重新加载,已成功取得的数据不因后续刷新失败消失。
设计共享 Message 处理负责统一展示与缓存页面生命周期;请求层保留业务错误码、HTTP 状态和 requestId,静默请求不会自行弹窗。未取得数据与空结果保持区分。
开发代表性覆盖内容制作、系统用户与教学实施,包括训练编辑器加载失败的重试入口。接口异常详情仅在 Message 中展示;表单必填错误、项目编码说明和离线运行模式继续保留。
测试使用真实 Vue / Element Plus 页面、Edge 无头浏览器和浏览器级 API mock。覆盖本机与局域网入口。身份及接口数据均为合成测试数据;不登录真实账号、不写业务库,不将本轮测试称为真实服务端或设备联调。
浏览器验证(主回归)
PASS
1. 内容列表:错误自动消失、重试恢复、再次失败保留旧数据
12.8 秒
01-content-error-message
02-content-old-data-retainedPASS
2. 系统用户:并行同错合并一条,重复搜索可再次提示
12.7 秒
03-users-grouped-message
04-users-retry-without-bannerPASS
3. 训练编辑器:加载失败提示消失后保留重新加载按钮
6.4 秒
05-legacy-training-message
06-legacy-training-retryPASS
4. 实装预览:任务上下文错误用 Message,离线与教员模式持续可见
6.5 秒
07-physical-context-message
08-physical-mode-preservedPASS
5. 新建训练:业务说明与必填校验保留,不发起创建请求
2.5 秒
09-create-validation-preservedPASS
6. 缓存列表:失活时晚到错误不弹出,返回不重放旧错误
1.9 秒
10-inactive-page-no-message
11-reactivated-no-stale-messagePASS
7. HTTP 合同:真实 Axios 超时中文化,403/409 元数据保留,静默请求不自动弹窗
1.9 秒
12-http-silent-errors
15-business-message-keeps-original-textPASS
8. 局域网教学列表:既有 ElMessage.error 使用全局关闭和自动消失配置
6.9 秒
13-lan-teaching-message
14-lan-teaching-after-dismissPASS
9. 模型桥接 DOM 合同:初次错误与晚写详情转为重试说明,ready 恢复交互
0.8 秒
16-model-bridge-neutral-retry
17-model-bridge-ready认证附加回归(独立统计)
共实际运行 5 项:3 项通过;2 项旧强制改密用例受当前路由契约阻断,未验证到原断言。主报告 9 项结果单独统计。
Edge headless,http://127.0.0.1:6180,认证 API 全部由 Playwright 拦截模拟;trace/video 关闭。
仅模拟认证接口与浏览器交互,不使用真实账号,不验证后端认证规则或真实密码修改。
未运行依赖真实账号的 auth.spec.ts 全量;仅更新其中接口错误提示的断言选择器。
两条旧强制改密用例没有通过,不能作为当前改密功能验证证据;既有路由策略不在本轮提示展示调整范围。
PASS
859:无可登录教员时账号选择器禁用
保留业务空态,选择器不可展开。
无教员业务空态BLOCKED
861:旧强制改密不一致校验
当前 /change-password 对已认证用户重定向 /dashboard;旧 fixture 只有强制改密标记而无工作台权限,后续导航取消,停留在启动页,未进入改密表单。该旧用例不适用于当前路由契约,本次未改路由、未跳过或记为通过。
Playwright 原始状态:failed。
旧改密入口阻断BLOCKED
864:旧强制改密停用账号提示
与 861 相同的既有路由契约阻断,未能提交初始改密接口,也未验证一次性登录提示。原始失败证据保留。
Playwright 原始状态:failed。
旧停用账号流程阻断PASS
普通会话过期不泄露后端内部详情
跳转登录页,登录错误栏为空,不显示内部会话或刷新令牌详情,仅发起一次刷新请求。
PASS
当前管理员登录 API 401 错误使用自动消失的 Message
实际点击登录;mock POST /auth/login 返回 401。只出现一条 .el-message--error,#login-error 始终为空;未点击关闭,Message 自动消失。登录请求 1 次,刷新请求 0 次。
登录失败 Message
Message 自动消失后构建与聚焦逻辑验证
PASS · unreal_tran_web
pnpm run build
最终 vue-tsc、Vite 与 legacy iframe public/dist 哈希校验通过;仍有既有 chunk 体积提示。 日志
PASS · ute2e
node --test tools/scene-cover-api.test.mjs tools/guide-error-message.test.mjs
20/20 通过,包含保存失败只提示一次、阻断发布与离开,以及指导书/场景错误提示回归。 日志
PASS · ute2e
pnpm exec tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --strict --types 'node,@playwright/test' tests/api-error-messages.spec.ts api-error-messages.config.ts
本轮新增浏览器测试与配置类型检查通过。
PASS · unreal_tran_web、ute2e
git diff --check
主任务检查通过;报告生成后再次核验本轮文件。
范围与复现
这是接口失败展示的聚焦浏览器回归,未逐一遍历所有业务页面。
页面渲染、Element Plus Message、缓存路由和 Axios 超时均运行于真实浏览器;服务端响应由测试模拟。
模型 iframe 项为真实宿主桥接的 DOM 合同测试,使用空画布夹具,不代表完整 GLB 模型加载测试。
保留训练业务状态、表单校验与重试入口。未扩大教学流程、权限或接口合同。
pnpm exec playwright test -c api-error-messages.config.ts
报告重新生成:node tools/build-api-error-message-report.mjs
\ No newline at end of file
diff --git a/reports/api-error-messages-20260906/playwright/data/00f52af7460dd91a453fcee86f535438b924886c.png b/reports/api-error-messages-20260906/playwright/data/00f52af7460dd91a453fcee86f535438b924886c.png
new file mode 100644
index 0000000..94bd489
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/00f52af7460dd91a453fcee86f535438b924886c.png differ
diff --git a/reports/api-error-messages-20260906/playwright/data/0f2eafc0ded875e82e42b7e809566dee5c2fb2cd.png b/reports/api-error-messages-20260906/playwright/data/0f2eafc0ded875e82e42b7e809566dee5c2fb2cd.png
new file mode 100644
index 0000000..2491173
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/0f2eafc0ded875e82e42b7e809566dee5c2fb2cd.png differ
diff --git a/reports/api-error-messages-20260906/playwright/data/17507cdbdd99dd7f0d9fd151ae290df25c7a0642.png b/reports/api-error-messages-20260906/playwright/data/17507cdbdd99dd7f0d9fd151ae290df25c7a0642.png
new file mode 100644
index 0000000..9ccbac1
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/17507cdbdd99dd7f0d9fd151ae290df25c7a0642.png differ
diff --git a/reports/api-error-messages-20260906/playwright/data/2813e75186e0fad9d37770a9ed59535edec099f4.png b/reports/api-error-messages-20260906/playwright/data/2813e75186e0fad9d37770a9ed59535edec099f4.png
new file mode 100644
index 0000000..ba86979
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/2813e75186e0fad9d37770a9ed59535edec099f4.png differ
diff --git a/reports/api-error-messages-20260906/playwright/data/2bd9442f53c5bcf42a8e5d1575cf86e60f09727b.png b/reports/api-error-messages-20260906/playwright/data/2bd9442f53c5bcf42a8e5d1575cf86e60f09727b.png
new file mode 100644
index 0000000..8a5e92c
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/2bd9442f53c5bcf42a8e5d1575cf86e60f09727b.png differ
diff --git a/reports/api-error-messages-20260906/playwright/data/3592c1eb0bc207597d80a118da25996fdf09ab67.png b/reports/api-error-messages-20260906/playwright/data/3592c1eb0bc207597d80a118da25996fdf09ab67.png
new file mode 100644
index 0000000..b6c5f5e
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/3592c1eb0bc207597d80a118da25996fdf09ab67.png differ
diff --git a/reports/api-error-messages-20260906/playwright/data/3c895f0ca75f16bf707237cf74af25adab2b8f61.png b/reports/api-error-messages-20260906/playwright/data/3c895f0ca75f16bf707237cf74af25adab2b8f61.png
new file mode 100644
index 0000000..77b55cd
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/3c895f0ca75f16bf707237cf74af25adab2b8f61.png differ
diff --git a/reports/api-error-messages-20260906/playwright/data/3de3e9e62a6c772cfa010aa73d55231e84271d60.png b/reports/api-error-messages-20260906/playwright/data/3de3e9e62a6c772cfa010aa73d55231e84271d60.png
new file mode 100644
index 0000000..0595481
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/3de3e9e62a6c772cfa010aa73d55231e84271d60.png differ
diff --git a/reports/api-error-messages-20260906/playwright/data/4e786aaa7c17ef4a09607c55f134c6be7d2d85b9.png b/reports/api-error-messages-20260906/playwright/data/4e786aaa7c17ef4a09607c55f134c6be7d2d85b9.png
new file mode 100644
index 0000000..3eaf707
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/4e786aaa7c17ef4a09607c55f134c6be7d2d85b9.png differ
diff --git a/reports/api-error-messages-20260906/playwright/data/67a05f8fe6a1471fd8f0c5ebf37698dcfa6f3ea1.png b/reports/api-error-messages-20260906/playwright/data/67a05f8fe6a1471fd8f0c5ebf37698dcfa6f3ea1.png
new file mode 100644
index 0000000..0da9ee9
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/67a05f8fe6a1471fd8f0c5ebf37698dcfa6f3ea1.png differ
diff --git a/reports/api-error-messages-20260906/playwright/data/791f083567254786a0d25d46f5990309148370fc.png b/reports/api-error-messages-20260906/playwright/data/791f083567254786a0d25d46f5990309148370fc.png
new file mode 100644
index 0000000..02d4676
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/791f083567254786a0d25d46f5990309148370fc.png differ
diff --git a/reports/api-error-messages-20260906/playwright/data/8c5e81abf643c9014dc8bcfc84cb1dfb48192ae5.png b/reports/api-error-messages-20260906/playwright/data/8c5e81abf643c9014dc8bcfc84cb1dfb48192ae5.png
new file mode 100644
index 0000000..a61e369
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/8c5e81abf643c9014dc8bcfc84cb1dfb48192ae5.png differ
diff --git a/reports/api-error-messages-20260906/playwright/data/b4922029a6ab37c56716c59f6aaa50e4d3189113.png b/reports/api-error-messages-20260906/playwright/data/b4922029a6ab37c56716c59f6aaa50e4d3189113.png
new file mode 100644
index 0000000..1a96d4a
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/b4922029a6ab37c56716c59f6aaa50e4d3189113.png differ
diff --git a/reports/api-error-messages-20260906/playwright/data/cebc174fab800fdfa4a213bc86d68f50d2971ad3.png b/reports/api-error-messages-20260906/playwright/data/cebc174fab800fdfa4a213bc86d68f50d2971ad3.png
new file mode 100644
index 0000000..cbd8b9d
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/cebc174fab800fdfa4a213bc86d68f50d2971ad3.png differ
diff --git a/reports/api-error-messages-20260906/playwright/data/d078cc25a7aec6aaa723751aaf8365a14afa242d.png b/reports/api-error-messages-20260906/playwright/data/d078cc25a7aec6aaa723751aaf8365a14afa242d.png
new file mode 100644
index 0000000..4599717
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/d078cc25a7aec6aaa723751aaf8365a14afa242d.png differ
diff --git a/reports/api-error-messages-20260906/playwright/data/d44504e5959e7f0e0f46a63bc5fee46707b0121d.png b/reports/api-error-messages-20260906/playwright/data/d44504e5959e7f0e0f46a63bc5fee46707b0121d.png
new file mode 100644
index 0000000..daead15
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/d44504e5959e7f0e0f46a63bc5fee46707b0121d.png differ
diff --git a/reports/api-error-messages-20260906/playwright/data/e225277a4b743cee1aaeba6b1471fb1a3e09b29f.png b/reports/api-error-messages-20260906/playwright/data/e225277a4b743cee1aaeba6b1471fb1a3e09b29f.png
new file mode 100644
index 0000000..d3fb731
Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/e225277a4b743cee1aaeba6b1471fb1a3e09b29f.png differ
diff --git a/reports/api-error-messages-20260906/playwright/index.html b/reports/api-error-messages-20260906/playwright/index.html
new file mode 100644
index 0000000..f0d0a10
--- /dev/null
+++ b/reports/api-error-messages-20260906/playwright/index.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+ Playwright Test Report
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/reports/api-error-messages-20260906/report.md b/reports/api-error-messages-20260906/report.md
new file mode 100644
index 0000000..6928cec
--- /dev/null
+++ b/reports/api-error-messages-20260906/report.md
@@ -0,0 +1,159 @@
+# 接口错误 Message 回归报告
+
+主回归:**9/9 通过**。17 张截图,58 次模拟 API 请求,0 个未捕获页面异常。
+
+认证附加:3 项通过,2 项旧改密入口受当前路由契约阻断。原始失败保留,未计为通过。总状态:PASS_WITH_LEGACY_BLOCKERS。
+
+## 资料
+
+按本轮“接口错误使用 Message,不保留常驻错误横条”的要求检查。沿用已确认的教学业务边界:离线识别回放、观察模式、步骤反馈和表单校验仍需可见。
+
+## 原型
+
+错误发生时顶部短暂提示;可以手动关闭,约 4.5 秒自动消失。同一错误合并展示;失败后仍可重新加载,已成功取得的数据不因后续刷新失败消失。
+
+## 设计
+
+共享 Message 处理负责统一展示与缓存页面生命周期;请求层保留业务错误码、HTTP 状态和 requestId,静默请求不会自行弹窗。未取得数据与空结果保持区分。
+
+## 开发
+
+代表性覆盖内容制作、系统用户与教学实施,包括训练编辑器加载失败的重试入口。接口异常详情仅在 Message 中展示;表单必填错误、项目编码说明和离线运行模式继续保留。
+
+## 测试
+
+使用真实 Vue / Element Plus 页面、Edge 无头浏览器和浏览器级 API mock。覆盖本机与局域网入口。身份及接口数据均为合成测试数据;不登录真实账号、不写业务库,不将本轮测试称为真实服务端或设备联调。
+
+## 浏览器结果
+
+### 1. 内容列表:错误自动消失、重试恢复、再次失败保留旧数据
+
+状态:PASS。耗时 12.8 秒。
+
+
+
+
+
+### 2. 系统用户:并行同错合并一条,重复搜索可再次提示
+
+状态:PASS。耗时 12.7 秒。
+
+
+
+
+
+### 3. 训练编辑器:加载失败提示消失后保留重新加载按钮
+
+状态:PASS。耗时 6.4 秒。
+
+
+
+
+
+### 4. 实装预览:任务上下文错误用 Message,离线与教员模式持续可见
+
+状态:PASS。耗时 6.5 秒。
+
+
+
+
+
+### 5. 新建训练:业务说明与必填校验保留,不发起创建请求
+
+状态:PASS。耗时 2.5 秒。
+
+
+
+### 6. 缓存列表:失活时晚到错误不弹出,返回不重放旧错误
+
+状态:PASS。耗时 1.9 秒。
+
+
+
+
+
+### 7. HTTP 合同:真实 Axios 超时中文化,403/409 元数据保留,静默请求不自动弹窗
+
+状态:PASS。耗时 1.9 秒。
+
+
+
+
+
+### 8. 局域网教学列表:既有 ElMessage.error 使用全局关闭和自动消失配置
+
+状态:PASS。耗时 6.9 秒。
+
+
+
+
+
+### 9. 模型桥接 DOM 合同:初次错误与晚写详情转为重试说明,ready 恢复交互
+
+状态:PASS。耗时 0.8 秒。
+
+
+
+
+
+## 认证附加回归(独立统计)
+
+共实际运行 5 项:3 项通过;2 项旧强制改密用例受当前路由契约阻断,未验证到原断言。主报告 9 项结果单独统计。
+
+### 859:无可登录教员时账号选择器禁用
+
+状态:PASS。保留业务空态,选择器不可展开。
+
+
+
+### 861:旧强制改密不一致校验
+
+状态:BLOCKED;Playwright 原始状态:failed。当前 /change-password 对已认证用户重定向 /dashboard;旧 fixture 只有强制改密标记而无工作台权限,后续导航取消,停留在启动页,未进入改密表单。该旧用例不适用于当前路由契约,本次未改路由、未跳过或记为通过。
+
+
+
+### 864:旧强制改密停用账号提示
+
+状态:BLOCKED;Playwright 原始状态:failed。与 861 相同的既有路由契约阻断,未能提交初始改密接口,也未验证一次性登录提示。原始失败证据保留。
+
+
+
+### 普通会话过期不泄露后端内部详情
+
+状态:PASS。跳转登录页,登录错误栏为空,不显示内部会话或刷新令牌详情,仅发起一次刷新请求。
+
+### 当前管理员登录 API 401 错误使用自动消失的 Message
+
+状态:PASS。实际点击登录;mock POST /auth/login 返回 401。只出现一条 .el-message--error,#login-error 始终为空;未点击关闭,Message 自动消失。登录请求 1 次,刷新请求 0 次。
+
+
+
+
+
+- 仅模拟认证接口与浏览器交互,不使用真实账号,不验证后端认证规则或真实密码修改。
+- 未运行依赖真实账号的 auth.spec.ts 全量;仅更新其中接口错误提示的断言选择器。
+- 两条旧强制改密用例没有通过,不能作为当前改密功能验证证据;既有路由策略不在本轮提示展示调整范围。
+
+[认证汇总证据](authentication/summary.json)
+
+## 范围与复现
+
+- 这是接口失败展示的聚焦浏览器回归,未逐一遍历所有业务页面。
+- 页面渲染、Element Plus Message、缓存路由和 Axios 超时均运行于真实浏览器;服务端响应由测试模拟。
+- 模型 iframe 项为真实宿主桥接的 DOM 合同测试,使用空画布夹具,不代表完整 GLB 模型加载测试。
+- 保留训练业务状态、表单校验与重试入口。未扩大教学流程、权限或接口合同。
+
+pnpm run build:PASS。最终 vue-tsc、Vite 与 legacy iframe public/dist 哈希校验通过;仍有既有 chunk 体积提示。
+
+node --test tools/scene-cover-api.test.mjs tools/guide-error-message.test.mjs:PASS。20/20 通过,包含保存失败只提示一次、阻断发布与离开,以及指导书/场景错误提示回归。
+
+pnpm exec tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --strict --types 'node,@playwright/test' tests/api-error-messages.spec.ts api-error-messages.config.ts:PASS。本轮新增浏览器测试与配置类型检查通过。
+
+git diff --check:PASS。主任务检查通过;报告生成后再次核验本轮文件。
+
+```powershell
+pnpm exec playwright test -c api-error-messages.config.ts
+node tools/build-api-error-message-report.mjs
+```
+
+测试开始:2026-09-05T17:32:19.650Z。报告生成:2026-09-05T17:36:20.720Z。
diff --git a/reports/api-error-messages-20260906/results.json b/reports/api-error-messages-20260906/results.json
new file mode 100644
index 0000000..c346585
--- /dev/null
+++ b/reports/api-error-messages-20260906/results.json
@@ -0,0 +1,556 @@
+{
+ "config": {
+ "configFile": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\api-error-messages.config.ts",
+ "rootDir": "E:/work/edit_3d/unreal_tran/ute2e/tests",
+ "forbidOnly": false,
+ "fullyParallel": false,
+ "globalSetup": null,
+ "globalTeardown": null,
+ "globalTimeout": 0,
+ "grep": {},
+ "grepInvert": null,
+ "maxFailures": 0,
+ "metadata": {
+ "actualWorkers": 1
+ },
+ "preserveOutput": "always",
+ "reporter": [
+ [
+ "list",
+ null
+ ],
+ [
+ "html",
+ {
+ "outputFolder": "reports/api-error-messages-20260906/playwright",
+ "open": "never"
+ }
+ ],
+ [
+ "json",
+ {
+ "outputFile": "reports/api-error-messages-20260906/results.json"
+ }
+ ]
+ ],
+ "reportSlowTests": {
+ "max": 5,
+ "threshold": 300000
+ },
+ "quiet": false,
+ "projects": [
+ {
+ "outputDir": "E:/work/edit_3d/unreal_tran/ute2e/reports/api-error-messages-20260906/test-results",
+ "repeatEach": 1,
+ "retries": 0,
+ "metadata": {
+ "actualWorkers": 1
+ },
+ "id": "",
+ "name": "",
+ "testDir": "E:/work/edit_3d/unreal_tran/ute2e/tests",
+ "testIgnore": [],
+ "testMatch": [
+ "api-error-messages.spec.ts"
+ ],
+ "timeout": 60000
+ }
+ ],
+ "shard": null,
+ "updateSnapshots": "missing",
+ "updateSourceMethod": "patch",
+ "version": "1.55.0",
+ "workers": 1,
+ "webServer": null
+ },
+ "suites": [
+ {
+ "title": "api-error-messages.spec.ts",
+ "file": "api-error-messages.spec.ts",
+ "column": 0,
+ "line": 0,
+ "specs": [
+ {
+ "title": "内容列表:错误自动消失、重试恢复、再次失败保留旧数据",
+ "ok": true,
+ "tags": [],
+ "tests": [
+ {
+ "timeout": 60000,
+ "annotations": [],
+ "expectedStatus": "passed",
+ "projectId": "",
+ "projectName": "",
+ "results": [
+ {
+ "workerIndex": 0,
+ "parallelIndex": 0,
+ "status": "passed",
+ "duration": 12754,
+ "errors": [],
+ "stdout": [],
+ "stderr": [],
+ "retry": 0,
+ "startTime": "2026-09-05T17:32:20.820Z",
+ "annotations": [],
+ "attachments": [
+ {
+ "name": "01-content-error-message",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-内容列表:错误自动消失、重试恢复、再次失败保留旧数据\\attachments\\01-content-error-message-ac439d2f66329cf28022531899833b96ec5b23da.png"
+ },
+ {
+ "name": "02-content-old-data-retained",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-内容列表:错误自动消失、重试恢复、再次失败保留旧数据\\attachments\\02-content-old-data-retained-0cbf8d445ed5097f84d18d5be7de9ba814a92620.png"
+ },
+ {
+ "name": "API 隔离与浏览器错误",
+ "contentType": "application/json",
+ "body": "ewogICJyZXF1ZXN0cyI6IFsKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvc3lzdGVtLWNvbmZpZy9wdWJsaWMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9hdXRoL21lIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvbWVudXMvbmF2aWdhdGlvbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMvc3VtbWFyeSIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvdHJhbi92MS9jb250ZW50L3Byb2plY3RzIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL3RyYW4vdjEvY29udGVudC9wcm9qZWN0cy9zdW1tYXJ5IgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL3RyYW4vdjEvY29udGVudC9wcm9qZWN0cyIKICAgIH0KICBdLAogICJ1bmV4cGVjdGVkIjogW10sCiAgIndyaXRlcyI6IFtdLAogICJwYWdlRXJyb3JzIjogW10KfQ=="
+ }
+ ]
+ }
+ ],
+ "status": "expected"
+ }
+ ],
+ "id": "a01d8293b6a423e3d18f-2038675176804df1ab00",
+ "file": "api-error-messages.spec.ts",
+ "line": 129,
+ "column": 1
+ },
+ {
+ "title": "系统用户:并行同错合并一条,重复搜索可再次提示",
+ "ok": true,
+ "tags": [],
+ "tests": [
+ {
+ "timeout": 60000,
+ "annotations": [],
+ "expectedStatus": "passed",
+ "projectId": "",
+ "projectName": "",
+ "results": [
+ {
+ "workerIndex": 0,
+ "parallelIndex": 0,
+ "status": "passed",
+ "duration": 12705,
+ "errors": [],
+ "stdout": [],
+ "stderr": [],
+ "retry": 0,
+ "steps": [
+ {
+ "title": "Expect \"poll toBe\"",
+ "duration": 1
+ }
+ ],
+ "startTime": "2026-09-05T17:32:34.098Z",
+ "annotations": [],
+ "attachments": [
+ {
+ "name": "03-users-grouped-message",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-系统用户:并行同错合并一条,重复搜索可再次提示\\attachments\\03-users-grouped-message-0575ee194c8ced5662a122dcc67aa169686ac8a6.png"
+ },
+ {
+ "name": "04-users-retry-without-banner",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-系统用户:并行同错合并一条,重复搜索可再次提示\\attachments\\04-users-retry-without-banner-e04f7c4569a80610fc43865d44b28f1d75fb8df9.png"
+ },
+ {
+ "name": "API 隔离与浏览器错误",
+ "contentType": "application/json",
+ "body": "ewogICJyZXF1ZXN0cyI6IFsKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvc3lzdGVtLWNvbmZpZy9wdWJsaWMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9hdXRoL21lIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvbWVudXMvbmF2aWdhdGlvbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS9hdXRoL3YxL3JvbGVzL2FsbCIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS9hdXRoL3YxL2RlcGFydG1lbnRzL3RyZWUiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS91c2VycyIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS9hdXRoL3YxL3VzZXJzL3N0YXRzIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvdXNlcnMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS91c2Vycy9zdGF0cyIKICAgIH0KICBdLAogICJ1bmV4cGVjdGVkIjogW10sCiAgIndyaXRlcyI6IFtdLAogICJwYWdlRXJyb3JzIjogW10KfQ=="
+ }
+ ]
+ }
+ ],
+ "status": "expected"
+ }
+ ],
+ "id": "a01d8293b6a423e3d18f-ff7858551a9850a38ecf",
+ "file": "api-error-messages.spec.ts",
+ "line": 151,
+ "column": 1
+ },
+ {
+ "title": "训练编辑器:加载失败提示消失后保留重新加载按钮",
+ "ok": true,
+ "tags": [],
+ "tests": [
+ {
+ "timeout": 60000,
+ "annotations": [],
+ "expectedStatus": "passed",
+ "projectId": "",
+ "projectName": "",
+ "results": [
+ {
+ "workerIndex": 0,
+ "parallelIndex": 0,
+ "status": "passed",
+ "duration": 6385,
+ "errors": [],
+ "stdout": [],
+ "stderr": [],
+ "retry": 0,
+ "startTime": "2026-09-05T17:32:46.827Z",
+ "annotations": [],
+ "attachments": [
+ {
+ "name": "05-legacy-training-message",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-训练编辑器:加载失败提示消失后保留重新加载按钮\\attachments\\05-legacy-training-message-c8e11a1d6b9a8f408d0a84d20b4223c775b7c413.png"
+ },
+ {
+ "name": "06-legacy-training-retry",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-训练编辑器:加载失败提示消失后保留重新加载按钮\\attachments\\06-legacy-training-retry-ecc6b9f71cbe20b889313296c4d899241b950a74.png"
+ },
+ {
+ "name": "API 隔离与浏览器错误",
+ "contentType": "application/json",
+ "body": "ewogICJyZXF1ZXN0cyI6IFsKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvc3lzdGVtLWNvbmZpZy9wdWJsaWMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9hdXRoL21lIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvbWVudXMvbmF2aWdhdGlvbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMvbWVzc2FnZS10cmFpbmluZy05MDEiCiAgICB9CiAgXSwKICAidW5leHBlY3RlZCI6IFtdLAogICJ3cml0ZXMiOiBbXSwKICAicGFnZUVycm9ycyI6IFtdCn0="
+ }
+ ]
+ }
+ ],
+ "status": "expected"
+ }
+ ],
+ "id": "a01d8293b6a423e3d18f-7ef94e791c36ee1ddedd",
+ "file": "api-error-messages.spec.ts",
+ "line": 169,
+ "column": 1
+ },
+ {
+ "title": "实装预览:任务上下文错误用 Message,离线与教员模式持续可见",
+ "ok": true,
+ "tags": [],
+ "tests": [
+ {
+ "timeout": 60000,
+ "annotations": [],
+ "expectedStatus": "passed",
+ "projectId": "",
+ "projectName": "",
+ "results": [
+ {
+ "workerIndex": 0,
+ "parallelIndex": 0,
+ "status": "passed",
+ "duration": 6450,
+ "errors": [],
+ "stdout": [],
+ "stderr": [],
+ "retry": 0,
+ "startTime": "2026-09-05T17:32:53.229Z",
+ "annotations": [],
+ "attachments": [
+ {
+ "name": "07-physical-context-message",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-实装预览:任务上下文错误用-Message,离线与教员模式持续可见\\attachments\\07-physical-context-message-63fb383ff8eca8d595add99a6198d4d2882e9aaf.png"
+ },
+ {
+ "name": "08-physical-mode-preserved",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-实装预览:任务上下文错误用-Message,离线与教员模式持续可见\\attachments\\08-physical-mode-preserved-350411ad11a7f8a6727bd50289e51b171f32550b.png"
+ },
+ {
+ "name": "API 隔离与浏览器错误",
+ "contentType": "application/json",
+ "body": "ewogICJyZXF1ZXN0cyI6IFsKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvc3lzdGVtLWNvbmZpZy9wdWJsaWMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9hdXRoL21lIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvbWVudXMvbmF2aWdhdGlvbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL3RlYWNoaW5nL2Fzc2lnbm1lbnRzL21vY2stcGh5c2ljYWwiCiAgICB9CiAgXSwKICAidW5leHBlY3RlZCI6IFtdLAogICJ3cml0ZXMiOiBbXSwKICAicGFnZUVycm9ycyI6IFtdCn0="
+ }
+ ]
+ }
+ ],
+ "status": "expected"
+ }
+ ],
+ "id": "a01d8293b6a423e3d18f-364cf753ab5214963c3e",
+ "file": "api-error-messages.spec.ts",
+ "line": 181,
+ "column": 1
+ },
+ {
+ "title": "新建训练:业务说明与必填校验保留,不发起创建请求",
+ "ok": true,
+ "tags": [],
+ "tests": [
+ {
+ "timeout": 60000,
+ "annotations": [],
+ "expectedStatus": "passed",
+ "projectId": "",
+ "projectName": "",
+ "results": [
+ {
+ "workerIndex": 0,
+ "parallelIndex": 0,
+ "status": "passed",
+ "duration": 2485,
+ "errors": [],
+ "stdout": [],
+ "stderr": [],
+ "retry": 0,
+ "startTime": "2026-09-05T17:32:59.693Z",
+ "annotations": [],
+ "attachments": [
+ {
+ "name": "09-create-validation-preserved",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-新建训练:业务说明与必填校验保留,不发起创建请求\\attachments\\09-create-validation-preserved-4c333aadab5e6c9da93d389a099dd07c104ac11f.png"
+ },
+ {
+ "name": "API 隔离与浏览器错误",
+ "contentType": "application/json",
+ "body": "ewogICJyZXF1ZXN0cyI6IFsKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvc3lzdGVtLWNvbmZpZy9wdWJsaWMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9hdXRoL21lIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvbWVudXMvbmF2aWdhdGlvbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMvc3VtbWFyeSIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMiCiAgICB9CiAgXSwKICAidW5leHBlY3RlZCI6IFtdLAogICJ3cml0ZXMiOiBbXSwKICAicGFnZUVycm9ycyI6IFtdCn0="
+ }
+ ]
+ }
+ ],
+ "status": "expected"
+ }
+ ],
+ "id": "a01d8293b6a423e3d18f-31297312774d5e164ef8",
+ "file": "api-error-messages.spec.ts",
+ "line": 195,
+ "column": 1
+ },
+ {
+ "title": "缓存列表:失活时晚到错误不弹出,返回不重放旧错误",
+ "ok": true,
+ "tags": [],
+ "tests": [
+ {
+ "timeout": 60000,
+ "annotations": [],
+ "expectedStatus": "passed",
+ "projectId": "",
+ "projectName": "",
+ "results": [
+ {
+ "workerIndex": 0,
+ "parallelIndex": 0,
+ "status": "passed",
+ "duration": 1947,
+ "errors": [],
+ "stdout": [],
+ "stderr": [],
+ "retry": 0,
+ "steps": [
+ {
+ "title": "Expect \"poll toBeGreaterThan\"",
+ "duration": 356
+ },
+ {
+ "title": "Expect \"poll toBe\"",
+ "duration": 17
+ }
+ ],
+ "startTime": "2026-09-05T17:33:02.191Z",
+ "annotations": [],
+ "attachments": [
+ {
+ "name": "10-inactive-page-no-message",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-缓存列表:失活时晚到错误不弹出,返回不重放旧错误\\attachments\\10-inactive-page-no-message-8860efdb888160db1c16587fccb41079a6a3face.png"
+ },
+ {
+ "name": "11-reactivated-no-stale-message",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-缓存列表:失活时晚到错误不弹出,返回不重放旧错误\\attachments\\11-reactivated-no-stale-message-d113250e56afaff950f860ca351e366466cfcdea.png"
+ },
+ {
+ "name": "API 隔离与浏览器错误",
+ "contentType": "application/json",
+ "body": "ewogICJyZXF1ZXN0cyI6IFsKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvc3lzdGVtLWNvbmZpZy9wdWJsaWMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9hdXRoL21lIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvbWVudXMvbmF2aWdhdGlvbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMvc3VtbWFyeSIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvdHJhbi92MS9jb250ZW50L3Byb2plY3RzL3N1bW1hcnkiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvdHJhbi92MS9jb250ZW50L3Byb2plY3RzIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvcm9sZXMvYWxsIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvZGVwYXJ0bWVudHMvdHJlZSIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS9hdXRoL3YxL3VzZXJzIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvdXNlcnMvc3RhdHMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvdHJhbi92MS9jb250ZW50L3Byb2plY3RzL3N1bW1hcnkiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvdHJhbi92MS9jb250ZW50L3Byb2plY3RzIgogICAgfQogIF0sCiAgInVuZXhwZWN0ZWQiOiBbXSwKICAid3JpdGVzIjogW10sCiAgInBhZ2VFcnJvcnMiOiBbXQp9"
+ }
+ ]
+ }
+ ],
+ "status": "expected"
+ }
+ ],
+ "id": "a01d8293b6a423e3d18f-4825b4e93934bda5a681",
+ "file": "api-error-messages.spec.ts",
+ "line": 206,
+ "column": 1
+ },
+ {
+ "title": "HTTP 合同:真实 Axios 超时中文化,403/409 元数据保留,静默请求不自动弹窗",
+ "ok": true,
+ "tags": [],
+ "tests": [
+ {
+ "timeout": 60000,
+ "annotations": [],
+ "expectedStatus": "passed",
+ "projectId": "",
+ "projectName": "",
+ "results": [
+ {
+ "workerIndex": 0,
+ "parallelIndex": 0,
+ "status": "passed",
+ "duration": 1942,
+ "errors": [],
+ "stdout": [],
+ "stderr": [],
+ "retry": 0,
+ "startTime": "2026-09-05T17:33:04.152Z",
+ "annotations": [],
+ "attachments": [
+ {
+ "name": "HTTP 合同证据",
+ "contentType": "application/json",
+ "body": "WwogIHsKICAgICJlbmRwb2ludCI6ICJ0aW1lb3V0IiwKICAgICJtZXNzYWdlIjogIuivt+axgui2heaXtu+8jOivt+eojeWQjumHjeivlSIsCiAgICAiY29kZSI6ICJORVRXT1JLX0VSUk9SIiwKICAgICJzdGF0dXMiOiBudWxsLAogICAgInJlcXVlc3RJZCI6ICIiCiAgfSwKICB7CiAgICAiZW5kcG9pbnQiOiAiZm9yYmlkZGVuIiwKICAgICJtZXNzYWdlIjogIuaXoOadg+iuv+mXrua1i+ivleaVsOaNriIsCiAgICAiY29kZSI6ICJBQ0NFU1NfREVOSUVEIiwKICAgICJzdGF0dXMiOiA0MDMsCiAgICAicmVxdWVzdElkIjogIm1vY2stNDAzIgogIH0sCiAgewogICAgImVuZHBvaW50IjogImNvbmZsaWN0IiwKICAgICJtZXNzYWdlIjogIueJiOacrOW3suWPmOWMlu+8jOivt+mHjeaWsOWKoOi9vSIsCiAgICAiY29kZSI6ICJWRVJTSU9OX0NPTkZMSUNUIiwKICAgICJzdGF0dXMiOiA0MDksCiAgICAicmVxdWVzdElkIjogIm1vY2stNDA5IgogIH0KXQ=="
+ },
+ {
+ "name": "12-http-silent-errors",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-HTTP-合同-39274-中文化,403-409-元数据保留,静默请求不自动弹窗\\attachments\\12-http-silent-errors-95eb3df2d354eee7c7755066bf80544b01b9e891.png"
+ },
+ {
+ "name": "15-business-message-keeps-original-text",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-HTTP-合同-39274-中文化,403-409-元数据保留,静默请求不自动弹窗\\attachments\\15-business-message-keeps-original-text-3e1f30e539433cd6d073a3a94a6b616d1b7f9691.png"
+ },
+ {
+ "name": "取消请求与业务原文证据",
+ "contentType": "application/json",
+ "body": "ewogICJjYW5jZWxsYXRpb24iOiB7CiAgICAibmFtZSI6ICJDYW5jZWxlZEVycm9yIiwKICAgICJjb2RlIjogIkVSUl9DQU5DRUxFRCIKICB9LAogICJidXNpbmVzc0Vycm9yIjogewogICAgIm1lc3NhZ2UiOiAiTmV0d29yayBFcnJvciIsCiAgICAiY29kZSI6ICJBU1NFVF9WQUxJREFUSU9OIiwKICAgICJyZXF1ZXN0SWQiOiAibW9jay1idXNpbmVzcyIsCiAgICAic3RhdHVzIjogNDAwCiAgfQp9"
+ },
+ {
+ "name": "API 隔离与浏览器错误",
+ "contentType": "application/json",
+ "body": "ewogICJyZXF1ZXN0cyI6IFsKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvc3lzdGVtLWNvbmZpZy9wdWJsaWMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9hdXRoL21lIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvbWVudXMvbmF2aWdhdGlvbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMvc3VtbWFyeSIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9fX21lc3NhZ2VfZTJlL3RpbWVvdXQiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9fX21lc3NhZ2VfZTJlL2ZvcmJpZGRlbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS9hdXRoL3YxL19fbWVzc2FnZV9lMmUvY29uZmxpY3QiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9fX21lc3NhZ2VfZTJlL3RpbWVvdXQiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9fX21lc3NhZ2VfZTJlL2J1c2luZXNzLXRleHQiCiAgICB9CiAgXSwKICAidW5leHBlY3RlZCI6IFtdLAogICJ3cml0ZXMiOiBbXSwKICAicGFnZUVycm9ycyI6IFtdCn0="
+ }
+ ]
+ }
+ ],
+ "status": "expected"
+ }
+ ],
+ "id": "a01d8293b6a423e3d18f-eae6f6dfb8332cc93a68",
+ "file": "api-error-messages.spec.ts",
+ "line": 235,
+ "column": 1
+ },
+ {
+ "title": "局域网教学列表:既有 ElMessage.error 使用全局关闭和自动消失配置",
+ "ok": true,
+ "tags": [],
+ "tests": [
+ {
+ "timeout": 60000,
+ "annotations": [],
+ "expectedStatus": "passed",
+ "projectId": "",
+ "projectName": "",
+ "results": [
+ {
+ "workerIndex": 0,
+ "parallelIndex": 0,
+ "status": "passed",
+ "duration": 6935,
+ "errors": [],
+ "stdout": [],
+ "stderr": [],
+ "retry": 0,
+ "startTime": "2026-09-05T17:33:06.107Z",
+ "annotations": [],
+ "attachments": [
+ {
+ "name": "13-lan-teaching-message",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-局域网教学列表:既有-ElMessage-error-使用全局关闭和自动消失配置\\attachments\\13-lan-teaching-message-bf868d286d097355c25d44a190d448de0fac2dcf.png"
+ },
+ {
+ "name": "14-lan-teaching-after-dismiss",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-局域网教学列表:既有-ElMessage-error-使用全局关闭和自动消失配置\\attachments\\14-lan-teaching-after-dismiss-cca08e9d67eaa7575f446c040ea339316e6f8b8b.png"
+ },
+ {
+ "name": "API 隔离与浏览器错误",
+ "contentType": "application/json",
+ "body": "ewogICJyZXF1ZXN0cyI6IFsKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvc3lzdGVtLWNvbmZpZy9wdWJsaWMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9hdXRoL21lIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvbWVudXMvbmF2aWdhdGlvbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL3RlYWNoaW5nL2Fzc2lnbm1lbnRzIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL3RyYW4vdjEvdGVhY2hpbmcvYXNzaWdubWVudHMvc3VtbWFyeSIKICAgIH0KICBdLAogICJ1bmV4cGVjdGVkIjogW10sCiAgIndyaXRlcyI6IFtdLAogICJwYWdlRXJyb3JzIjogW10KfQ=="
+ }
+ ]
+ }
+ ],
+ "status": "expected"
+ }
+ ],
+ "id": "a01d8293b6a423e3d18f-bc26053607f6ace6ff92",
+ "file": "api-error-messages.spec.ts",
+ "line": 295,
+ "column": 1
+ },
+ {
+ "title": "模型桥接 DOM 合同:初次错误与晚写详情转为重试说明,ready 恢复交互",
+ "ok": true,
+ "tags": [],
+ "tests": [
+ {
+ "timeout": 60000,
+ "annotations": [],
+ "expectedStatus": "passed",
+ "projectId": "",
+ "projectName": "",
+ "results": [
+ {
+ "workerIndex": 0,
+ "parallelIndex": 0,
+ "status": "passed",
+ "duration": 791,
+ "errors": [],
+ "stdout": [],
+ "stderr": [],
+ "retry": 0,
+ "startTime": "2026-09-05T17:33:13.059Z",
+ "annotations": [],
+ "attachments": [
+ {
+ "name": "16-model-bridge-neutral-retry",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-模型桥接-DOM-合同:初次错误与晚写详情转为重试说明,ready-恢复交互\\attachments\\16-model-bridge-neutral-retry-9e8444c1b83af1d3b6be9d12b894d0ced4d17299.png"
+ },
+ {
+ "name": "17-model-bridge-ready",
+ "contentType": "image/png",
+ "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-模型桥接-DOM-合同:初次错误与晚写详情转为重试说明,ready-恢复交互\\attachments\\17-model-bridge-ready-80cdec397323585143136b2d8ae522630c30257e.png"
+ },
+ {
+ "name": "API 隔离与浏览器错误",
+ "contentType": "application/json",
+ "body": "ewogICJyZXF1ZXN0cyI6IFtdLAogICJ1bmV4cGVjdGVkIjogW10sCiAgIndyaXRlcyI6IFtdLAogICJwYWdlRXJyb3JzIjogW10KfQ=="
+ }
+ ]
+ }
+ ],
+ "status": "expected"
+ }
+ ],
+ "id": "a01d8293b6a423e3d18f-c8d259ab5093a7d5f9c8",
+ "file": "api-error-messages.spec.ts",
+ "line": 307,
+ "column": 1
+ }
+ ]
+ }
+ ],
+ "errors": [],
+ "stats": {
+ "startTime": "2026-09-05T17:32:19.650Z",
+ "duration": 84090.698,
+ "expected": 9,
+ "skipped": 0,
+ "unexpected": 0,
+ "flaky": 0
+ }
+}
\ No newline at end of file
diff --git a/reports/api-error-messages-20260906/screenshots/01-content-error-message.png b/reports/api-error-messages-20260906/screenshots/01-content-error-message.png
new file mode 100644
index 0000000..635c767
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/01-content-error-message.png differ
diff --git a/reports/api-error-messages-20260906/screenshots/02-content-old-data-retained.png b/reports/api-error-messages-20260906/screenshots/02-content-old-data-retained.png
new file mode 100644
index 0000000..fb89ff7
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/02-content-old-data-retained.png differ
diff --git a/reports/api-error-messages-20260906/screenshots/03-users-grouped-message.png b/reports/api-error-messages-20260906/screenshots/03-users-grouped-message.png
new file mode 100644
index 0000000..2491173
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/03-users-grouped-message.png differ
diff --git a/reports/api-error-messages-20260906/screenshots/04-users-retry-without-banner.png b/reports/api-error-messages-20260906/screenshots/04-users-retry-without-banner.png
new file mode 100644
index 0000000..8a5e92c
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/04-users-retry-without-banner.png differ
diff --git a/reports/api-error-messages-20260906/screenshots/05-legacy-training-message.png b/reports/api-error-messages-20260906/screenshots/05-legacy-training-message.png
new file mode 100644
index 0000000..0595481
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/05-legacy-training-message.png differ
diff --git a/reports/api-error-messages-20260906/screenshots/06-legacy-training-retry.png b/reports/api-error-messages-20260906/screenshots/06-legacy-training-retry.png
new file mode 100644
index 0000000..4599717
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/06-legacy-training-retry.png differ
diff --git a/reports/api-error-messages-20260906/screenshots/07-physical-context-message.png b/reports/api-error-messages-20260906/screenshots/07-physical-context-message.png
new file mode 100644
index 0000000..9ccbac1
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/07-physical-context-message.png differ
diff --git a/reports/api-error-messages-20260906/screenshots/08-physical-mode-preserved.png b/reports/api-error-messages-20260906/screenshots/08-physical-mode-preserved.png
new file mode 100644
index 0000000..0da9ee9
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/08-physical-mode-preserved.png differ
diff --git a/reports/api-error-messages-20260906/screenshots/09-create-validation-preserved.png b/reports/api-error-messages-20260906/screenshots/09-create-validation-preserved.png
new file mode 100644
index 0000000..02d4676
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/09-create-validation-preserved.png differ
diff --git a/reports/api-error-messages-20260906/screenshots/10-inactive-page-no-message.png b/reports/api-error-messages-20260906/screenshots/10-inactive-page-no-message.png
new file mode 100644
index 0000000..1a96d4a
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/10-inactive-page-no-message.png differ
diff --git a/reports/api-error-messages-20260906/screenshots/11-reactivated-no-stale-message.png b/reports/api-error-messages-20260906/screenshots/11-reactivated-no-stale-message.png
new file mode 100644
index 0000000..ba86979
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/11-reactivated-no-stale-message.png differ
diff --git a/reports/api-error-messages-20260906/screenshots/12-http-silent-errors.png b/reports/api-error-messages-20260906/screenshots/12-http-silent-errors.png
new file mode 100644
index 0000000..77b55cd
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/12-http-silent-errors.png differ
diff --git a/reports/api-error-messages-20260906/screenshots/13-lan-teaching-message.png b/reports/api-error-messages-20260906/screenshots/13-lan-teaching-message.png
new file mode 100644
index 0000000..b6c5f5e
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/13-lan-teaching-message.png differ
diff --git a/reports/api-error-messages-20260906/screenshots/14-lan-teaching-after-dismiss.png b/reports/api-error-messages-20260906/screenshots/14-lan-teaching-after-dismiss.png
new file mode 100644
index 0000000..94bd489
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/14-lan-teaching-after-dismiss.png differ
diff --git a/reports/api-error-messages-20260906/screenshots/15-business-message-keeps-original-text.png b/reports/api-error-messages-20260906/screenshots/15-business-message-keeps-original-text.png
new file mode 100644
index 0000000..a61e369
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/15-business-message-keeps-original-text.png differ
diff --git a/reports/api-error-messages-20260906/screenshots/16-model-bridge-neutral-retry.png b/reports/api-error-messages-20260906/screenshots/16-model-bridge-neutral-retry.png
new file mode 100644
index 0000000..d3fb731
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/16-model-bridge-neutral-retry.png differ
diff --git a/reports/api-error-messages-20260906/screenshots/17-model-bridge-ready.png b/reports/api-error-messages-20260906/screenshots/17-model-bridge-ready.png
new file mode 100644
index 0000000..daead15
Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/17-model-bridge-ready.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-HTTP-合同-39274-中文化,403-409-元数据保留,静默请求不自动弹窗/attachments/12-http-silent-errors-95eb3df2d354eee7c7755066bf80544b01b9e891.png b/reports/api-error-messages-20260906/test-results/api-error-messages-HTTP-合同-39274-中文化,403-409-元数据保留,静默请求不自动弹窗/attachments/12-http-silent-errors-95eb3df2d354eee7c7755066bf80544b01b9e891.png
new file mode 100644
index 0000000..77b55cd
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-HTTP-合同-39274-中文化,403-409-元数据保留,静默请求不自动弹窗/attachments/12-http-silent-errors-95eb3df2d354eee7c7755066bf80544b01b9e891.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-HTTP-合同-39274-中文化,403-409-元数据保留,静默请求不自动弹窗/attachments/15-business-message-keeps-original-text-3e1f30e539433cd6d073a3a94a6b616d1b7f9691.png b/reports/api-error-messages-20260906/test-results/api-error-messages-HTTP-合同-39274-中文化,403-409-元数据保留,静默请求不自动弹窗/attachments/15-business-message-keeps-original-text-3e1f30e539433cd6d073a3a94a6b616d1b7f9691.png
new file mode 100644
index 0000000..a61e369
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-HTTP-合同-39274-中文化,403-409-元数据保留,静默请求不自动弹窗/attachments/15-business-message-keeps-original-text-3e1f30e539433cd6d073a3a94a6b616d1b7f9691.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-内容列表:错误自动消失、重试恢复、再次失败保留旧数据/attachments/01-content-error-message-ac439d2f66329cf28022531899833b96ec5b23da.png b/reports/api-error-messages-20260906/test-results/api-error-messages-内容列表:错误自动消失、重试恢复、再次失败保留旧数据/attachments/01-content-error-message-ac439d2f66329cf28022531899833b96ec5b23da.png
new file mode 100644
index 0000000..cbd8b9d
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-内容列表:错误自动消失、重试恢复、再次失败保留旧数据/attachments/01-content-error-message-ac439d2f66329cf28022531899833b96ec5b23da.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-内容列表:错误自动消失、重试恢复、再次失败保留旧数据/attachments/02-content-old-data-retained-0cbf8d445ed5097f84d18d5be7de9ba814a92620.png b/reports/api-error-messages-20260906/test-results/api-error-messages-内容列表:错误自动消失、重试恢复、再次失败保留旧数据/attachments/02-content-old-data-retained-0cbf8d445ed5097f84d18d5be7de9ba814a92620.png
new file mode 100644
index 0000000..3eaf707
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-内容列表:错误自动消失、重试恢复、再次失败保留旧数据/attachments/02-content-old-data-retained-0cbf8d445ed5097f84d18d5be7de9ba814a92620.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-实装预览:任务上下文错误用-Message,离线与教员模式持续可见/attachments/07-physical-context-message-63fb383ff8eca8d595add99a6198d4d2882e9aaf.png b/reports/api-error-messages-20260906/test-results/api-error-messages-实装预览:任务上下文错误用-Message,离线与教员模式持续可见/attachments/07-physical-context-message-63fb383ff8eca8d595add99a6198d4d2882e9aaf.png
new file mode 100644
index 0000000..9ccbac1
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-实装预览:任务上下文错误用-Message,离线与教员模式持续可见/attachments/07-physical-context-message-63fb383ff8eca8d595add99a6198d4d2882e9aaf.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-实装预览:任务上下文错误用-Message,离线与教员模式持续可见/attachments/08-physical-mode-preserved-350411ad11a7f8a6727bd50289e51b171f32550b.png b/reports/api-error-messages-20260906/test-results/api-error-messages-实装预览:任务上下文错误用-Message,离线与教员模式持续可见/attachments/08-physical-mode-preserved-350411ad11a7f8a6727bd50289e51b171f32550b.png
new file mode 100644
index 0000000..0da9ee9
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-实装预览:任务上下文错误用-Message,离线与教员模式持续可见/attachments/08-physical-mode-preserved-350411ad11a7f8a6727bd50289e51b171f32550b.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-局域网教学列表:既有-ElMessage-error-使用全局关闭和自动消失配置/attachments/13-lan-teaching-message-bf868d286d097355c25d44a190d448de0fac2dcf.png b/reports/api-error-messages-20260906/test-results/api-error-messages-局域网教学列表:既有-ElMessage-error-使用全局关闭和自动消失配置/attachments/13-lan-teaching-message-bf868d286d097355c25d44a190d448de0fac2dcf.png
new file mode 100644
index 0000000..b6c5f5e
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-局域网教学列表:既有-ElMessage-error-使用全局关闭和自动消失配置/attachments/13-lan-teaching-message-bf868d286d097355c25d44a190d448de0fac2dcf.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-局域网教学列表:既有-ElMessage-error-使用全局关闭和自动消失配置/attachments/14-lan-teaching-after-dismiss-cca08e9d67eaa7575f446c040ea339316e6f8b8b.png b/reports/api-error-messages-20260906/test-results/api-error-messages-局域网教学列表:既有-ElMessage-error-使用全局关闭和自动消失配置/attachments/14-lan-teaching-after-dismiss-cca08e9d67eaa7575f446c040ea339316e6f8b8b.png
new file mode 100644
index 0000000..94bd489
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-局域网教学列表:既有-ElMessage-error-使用全局关闭和自动消失配置/attachments/14-lan-teaching-after-dismiss-cca08e9d67eaa7575f446c040ea339316e6f8b8b.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-新建训练:业务说明与必填校验保留,不发起创建请求/attachments/09-create-validation-preserved-4c333aadab5e6c9da93d389a099dd07c104ac11f.png b/reports/api-error-messages-20260906/test-results/api-error-messages-新建训练:业务说明与必填校验保留,不发起创建请求/attachments/09-create-validation-preserved-4c333aadab5e6c9da93d389a099dd07c104ac11f.png
new file mode 100644
index 0000000..02d4676
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-新建训练:业务说明与必填校验保留,不发起创建请求/attachments/09-create-validation-preserved-4c333aadab5e6c9da93d389a099dd07c104ac11f.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-模型桥接-DOM-合同:初次错误与晚写详情转为重试说明,ready-恢复交互/attachments/16-model-bridge-neutral-retry-9e8444c1b83af1d3b6be9d12b894d0ced4d17299.png b/reports/api-error-messages-20260906/test-results/api-error-messages-模型桥接-DOM-合同:初次错误与晚写详情转为重试说明,ready-恢复交互/attachments/16-model-bridge-neutral-retry-9e8444c1b83af1d3b6be9d12b894d0ced4d17299.png
new file mode 100644
index 0000000..d3fb731
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-模型桥接-DOM-合同:初次错误与晚写详情转为重试说明,ready-恢复交互/attachments/16-model-bridge-neutral-retry-9e8444c1b83af1d3b6be9d12b894d0ced4d17299.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-模型桥接-DOM-合同:初次错误与晚写详情转为重试说明,ready-恢复交互/attachments/17-model-bridge-ready-80cdec397323585143136b2d8ae522630c30257e.png b/reports/api-error-messages-20260906/test-results/api-error-messages-模型桥接-DOM-合同:初次错误与晚写详情转为重试说明,ready-恢复交互/attachments/17-model-bridge-ready-80cdec397323585143136b2d8ae522630c30257e.png
new file mode 100644
index 0000000..daead15
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-模型桥接-DOM-合同:初次错误与晚写详情转为重试说明,ready-恢复交互/attachments/17-model-bridge-ready-80cdec397323585143136b2d8ae522630c30257e.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-系统用户:并行同错合并一条,重复搜索可再次提示/attachments/03-users-grouped-message-0575ee194c8ced5662a122dcc67aa169686ac8a6.png b/reports/api-error-messages-20260906/test-results/api-error-messages-系统用户:并行同错合并一条,重复搜索可再次提示/attachments/03-users-grouped-message-0575ee194c8ced5662a122dcc67aa169686ac8a6.png
new file mode 100644
index 0000000..2491173
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-系统用户:并行同错合并一条,重复搜索可再次提示/attachments/03-users-grouped-message-0575ee194c8ced5662a122dcc67aa169686ac8a6.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-系统用户:并行同错合并一条,重复搜索可再次提示/attachments/04-users-retry-without-banner-e04f7c4569a80610fc43865d44b28f1d75fb8df9.png b/reports/api-error-messages-20260906/test-results/api-error-messages-系统用户:并行同错合并一条,重复搜索可再次提示/attachments/04-users-retry-without-banner-e04f7c4569a80610fc43865d44b28f1d75fb8df9.png
new file mode 100644
index 0000000..8a5e92c
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-系统用户:并行同错合并一条,重复搜索可再次提示/attachments/04-users-retry-without-banner-e04f7c4569a80610fc43865d44b28f1d75fb8df9.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-缓存列表:失活时晚到错误不弹出,返回不重放旧错误/attachments/10-inactive-page-no-message-8860efdb888160db1c16587fccb41079a6a3face.png b/reports/api-error-messages-20260906/test-results/api-error-messages-缓存列表:失活时晚到错误不弹出,返回不重放旧错误/attachments/10-inactive-page-no-message-8860efdb888160db1c16587fccb41079a6a3face.png
new file mode 100644
index 0000000..1a96d4a
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-缓存列表:失活时晚到错误不弹出,返回不重放旧错误/attachments/10-inactive-page-no-message-8860efdb888160db1c16587fccb41079a6a3face.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-缓存列表:失活时晚到错误不弹出,返回不重放旧错误/attachments/11-reactivated-no-stale-message-d113250e56afaff950f860ca351e366466cfcdea.png b/reports/api-error-messages-20260906/test-results/api-error-messages-缓存列表:失活时晚到错误不弹出,返回不重放旧错误/attachments/11-reactivated-no-stale-message-d113250e56afaff950f860ca351e366466cfcdea.png
new file mode 100644
index 0000000..ba86979
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-缓存列表:失活时晚到错误不弹出,返回不重放旧错误/attachments/11-reactivated-no-stale-message-d113250e56afaff950f860ca351e366466cfcdea.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-训练编辑器:加载失败提示消失后保留重新加载按钮/attachments/05-legacy-training-message-c8e11a1d6b9a8f408d0a84d20b4223c775b7c413.png b/reports/api-error-messages-20260906/test-results/api-error-messages-训练编辑器:加载失败提示消失后保留重新加载按钮/attachments/05-legacy-training-message-c8e11a1d6b9a8f408d0a84d20b4223c775b7c413.png
new file mode 100644
index 0000000..0595481
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-训练编辑器:加载失败提示消失后保留重新加载按钮/attachments/05-legacy-training-message-c8e11a1d6b9a8f408d0a84d20b4223c775b7c413.png differ
diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-训练编辑器:加载失败提示消失后保留重新加载按钮/attachments/06-legacy-training-retry-ecc6b9f71cbe20b889313296c4d899241b950a74.png b/reports/api-error-messages-20260906/test-results/api-error-messages-训练编辑器:加载失败提示消失后保留重新加载按钮/attachments/06-legacy-training-retry-ecc6b9f71cbe20b889313296c4d899241b950a74.png
new file mode 100644
index 0000000..4599717
Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-训练编辑器:加载失败提示消失后保留重新加载按钮/attachments/06-legacy-training-retry-ecc6b9f71cbe20b889313296c4d899241b950a74.png differ
diff --git a/reports/api-error-messages-20260906/validation.json b/reports/api-error-messages-20260906/validation.json
new file mode 100644
index 0000000..b4977d4
--- /dev/null
+++ b/reports/api-error-messages-20260906/validation.json
@@ -0,0 +1,30 @@
+{
+ "commands": [
+ {
+ "command": "pnpm run build",
+ "scope": "unreal_tran_web",
+ "status": "PASS",
+ "details": "最终 vue-tsc、Vite 与 legacy iframe public/dist 哈希校验通过;仍有既有 chunk 体积提示。",
+ "log": "validation/build.log"
+ },
+ {
+ "command": "node --test tools/scene-cover-api.test.mjs tools/guide-error-message.test.mjs",
+ "scope": "ute2e",
+ "status": "PASS",
+ "details": "20/20 通过,包含保存失败只提示一次、阻断发布与离开,以及指导书/场景错误提示回归。",
+ "log": "validation/runtime-tests.log"
+ },
+ {
+ "command": "pnpm exec tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --strict --types 'node,@playwright/test' tests/api-error-messages.spec.ts api-error-messages.config.ts",
+ "scope": "ute2e",
+ "status": "PASS",
+ "details": "本轮新增浏览器测试与配置类型检查通过。"
+ },
+ {
+ "command": "git diff --check",
+ "scope": "unreal_tran_web、ute2e",
+ "status": "PASS",
+ "details": "主任务检查通过;报告生成后再次核验本轮文件。"
+ }
+ ]
+}
diff --git a/reports/api-error-messages-20260906/validation/build.log b/reports/api-error-messages-20260906/validation/build.log
new file mode 100644
index 0000000..7d3c4d7
Binary files /dev/null and b/reports/api-error-messages-20260906/validation/build.log differ
diff --git a/reports/api-error-messages-20260906/validation/runtime-tests.log b/reports/api-error-messages-20260906/validation/runtime-tests.log
new file mode 100644
index 0000000..c8464f5
Binary files /dev/null and b/reports/api-error-messages-20260906/validation/runtime-tests.log differ
diff --git a/tests/api-error-messages.spec.ts b/tests/api-error-messages.spec.ts
new file mode 100644
index 0000000..ac0078d
--- /dev/null
+++ b/tests/api-error-messages.spec.ts
@@ -0,0 +1,337 @@
+import { expect, test as base, type Page, type Route, type TestInfo } from '@playwright/test'
+import fs from 'node:fs'
+import path from 'node:path'
+
+/** UI/error-contract regression only. Synthetic authentication and API responses;
+ * no real accounts, real API writes, or training execution are involved.
+ */
+type Json = Record
+const ROOT = path.resolve('reports/api-error-messages-20260906')
+const SHOTS = path.join(ROOT, 'screenshots')
+fs.mkdirSync(SHOTS, { recursive: true })
+
+const permissions = [
+ 'content.training', 'content.model', 'content.scene', 'content.ofd', 'content.create', 'content.update', 'content.publish',
+ 'system.users', 'system.users.create', 'system.roles', 'system.departments',
+ 'teaching.physical', 'teaching.virtual', 'teaching.confrontation', 'teaching.tasks.create', 'teaching.tasks.publish',
+]
+const roles = [
+ { id: 'mock-teacher', code: 'teacher', name: '教员', shortName: '教', status: 1, builtIn: 1, isSuperAdmin: 0, dataScopeCode: 'ALL', permissions },
+ { id: 'mock-admin', code: 'admin', name: '系统管理员', shortName: '管', status: 1, builtIn: 1, isSuperAdmin: 0, dataScopeCode: 'ALL', permissions },
+]
+const envelope = (data: unknown) => ({ code: 200, message: 'OK', data, timestamp: Date.now(), requestId: 'mock-message-regression' })
+const ok = (route: Route, data: unknown) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(envelope(data)) })
+const fail = (route: Route, message: string, status = 503, code: number | string = 'MOCK_UNAVAILABLE', requestId = 'mock-message-regression') => route.fulfill({
+ status, contentType: 'application/json', body: JSON.stringify({ code, message, data: null, timestamp: Date.now(), requestId }),
+})
+const pageOf = (records: unknown[]) => ({ records, total: records.length, page: 1, size: 10 })
+const project = {
+ id: 'message-training-901', type: 'TRAINING', code: 'MESSAGE-REGRESSION-901', name: 'Message 回归训练',
+ description: '浏览器隔离测试数据', categoryCode: 'MEDIUM_REPAIR', coverUri: '', status: 'DRAFT',
+ trainingMode: 'VIRTUAL', version: 1, currentVersionId: 'mock-version', currentVersionNo: 1,
+ currentVersionStatus: 'DRAFT', ownerUserId: 'mock-user', ownerName: '测试教员', departmentId: 'mock-department',
+ addTime: 1788624000, updateTime: 1788624000, dependencyCount: 0, assetCount: 0,
+ content: { level: '初级', durationMinutes: 10, totalScore: 100, steps: [] },
+}
+
+class ApiMock {
+ projectError = ''
+ projectDetailError = ''
+ systemError = ''
+ assignmentError = ''
+ assignmentDetailError = ''
+ holdProjects = false
+ readonly heldProjects: Route[] = []
+ readonly requests: Array<{ method: string; path: string }> = []
+ readonly unexpected: string[] = []
+ readonly writes: string[] = []
+ readonly pageErrors: string[] = []
+
+ async install(page: Page) {
+ page.on('pageerror', error => this.pageErrors.push(error.message))
+ await page.addInitScript(() => {
+ sessionStorage.setItem('unreal-tran:web:access-token:v1', 'synthetic-message-test-token-not-valid')
+ })
+ await page.route(url => url.pathname.startsWith('/api/'), async route => {
+ const request = route.request()
+ const target = new URL(request.url()).pathname
+ this.requests.push({ method: request.method(), path: target })
+ if (request.method() !== 'GET') {
+ this.writes.push(`${request.method()} ${target}`)
+ return fail(route, '测试禁止业务写入', 405)
+ }
+ if (target === '/api/auth/v1/auth/me') return ok(route, {
+ user: { id: 'mock-user', username: 'message.regression', displayName: '消息回归测试', departmentId: 'mock-department', departmentName: '测试部门', mustChangePassword: false, version: 1 },
+ roles, activeRoleId: 'mock-teacher', permissions, authorizationMode: 'UNION', loginTime: 1788624000,
+ })
+ if (target === '/api/auth/v1/menus/navigation') return ok(route, [])
+ if (target === '/api/auth/v1/system-config/public') return ok(route, {
+ systemName: '装备数字车间', loginDescription: '', defaultTheme: 'light', digitalHumanWidget: null,
+ })
+ if (target === '/api/tran/v1/content/projects/summary') return ok(route, { total: 1, draft: 1, review: 0, published: 0, byType: {}, byTrainingMode: { VIRTUAL: { total: 1, draft: 1 } } })
+ if (target === '/api/tran/v1/content/projects') {
+ if (this.holdProjects) { this.heldProjects.push(route); return }
+ return this.projectError ? fail(route, this.projectError) : ok(route, pageOf([project]))
+ }
+ if (target === '/api/tran/v1/content/projects/message-training-901') return fail(route, this.projectDetailError || '测试工程详情不可用')
+ if (target === '/api/tran/v1/content/catalog') return ok(route, pageOf([]))
+ if (['/api/auth/v1/users', '/api/auth/v1/users/stats', '/api/auth/v1/departments/tree', '/api/auth/v1/roles/all'].includes(target)) {
+ if (this.systemError) return fail(route, this.systemError)
+ if (target.endsWith('/users')) return ok(route, pageOf([]))
+ if (target.endsWith('/stats')) return ok(route, { total: 0, enabled: 0, disabled: 0, passwordPending: 0 })
+ if (target.endsWith('/roles/all')) return ok(route, roles)
+ return ok(route, [{ id: 'mock-department', name: '测试部门', parentId: null, status: 1, children: [] }])
+ }
+ if (target === '/api/tran/v1/teaching/assignments') return this.assignmentError ? fail(route, this.assignmentError) : ok(route, pageOf([]))
+ if (target === '/api/tran/v1/teaching/assignments/summary') return ok(route, { total: 0, draft: 0, published: 0, active: 0, completed: 0, archived: 0, byChannel: {} })
+ if (target === '/api/tran/v1/teaching/runs') return ok(route, pageOf([]))
+ if (target === '/api/tran/v1/teaching/assignments/mock-physical') return fail(route, this.assignmentDetailError || '测试任务上下文不可用')
+ if (target.startsWith('/api/auth/v1/__message_e2e/')) {
+ if (target.endsWith('/timeout')) {
+ await new Promise(resolve => setTimeout(resolve, 400))
+ return ok(route, {}).catch(() => undefined)
+ }
+ if (target.endsWith('/forbidden')) return fail(route, '无权访问测试数据', 403, 'ACCESS_DENIED', 'mock-403')
+ if (target.endsWith('/conflict')) return fail(route, '版本已变化,请重新加载', 409, 'VERSION_CONFLICT', 'mock-409')
+ if (target.endsWith('/business-text')) return fail(route, 'Network Error', 400, 'ASSET_VALIDATION', 'mock-business')
+ }
+ this.unexpected.push(target)
+ return fail(route, '测试未覆盖的接口', 404)
+ })
+ }
+}
+
+const test = base.extend<{ api: ApiMock }>({
+ api: [async ({ page }, use) => {
+ const api = new ApiMock()
+ await api.install(page)
+ await use(api)
+ }, { auto: true }],
+})
+
+const shot = async (page: Page, info: TestInfo, name: string) => {
+ const target = path.join(SHOTS, `${name}.png`)
+ await page.screenshot({ path: target, fullPage: true, animations: 'disabled' })
+ await info.attach(name, { path: target, contentType: 'image/png' })
+}
+const message = (page: Page, text: string) => page.locator('.el-message--error').filter({ hasText: text })
+const noPersistentError = async (page: Page, text: string) => {
+ await expect(page.locator('.el-alert--error')).toHaveCount(0)
+ await expect(page.locator('body')).not.toContainText(text)
+}
+test.afterEach(async ({ api }, info) => {
+ await info.attach('API 隔离与浏览器错误', { body: JSON.stringify({ requests: api.requests, unexpected: api.unexpected, writes: api.writes, pageErrors: api.pageErrors }, null, 2), contentType: 'application/json' })
+ expect(api.unexpected, '所有 API 请求都有明确 mock 合同').toEqual([])
+ expect(api.writes, '没有发起真实或模拟业务写入').toEqual([])
+ expect(api.pageErrors, '没有未捕获浏览器异常').toEqual([])
+})
+
+test('内容列表:错误自动消失、重试恢复、再次失败保留旧数据', async ({ page, api }, info) => {
+ const text = '测试:训练列表服务暂不可用'
+ api.projectError = text
+ await page.goto('/content/training-projects')
+ await expect(message(page, text)).toBeVisible()
+ await expect(message(page, text).locator('.el-message__closeBtn')).toBeVisible()
+ await shot(page, info, '01-content-error-message')
+ await expect(message(page, text)).toBeHidden({ timeout: 8_000 })
+ await noPersistentError(page, text)
+ await expect(page.getByRole('button', { name: '重新加载', exact: true })).toBeVisible()
+ api.projectError = ''
+ await page.getByRole('button', { name: '重新加载', exact: true }).click()
+ await expect(page.locator('.training-project-card')).toContainText(project.name)
+ api.projectError = text
+ await page.getByRole('button', { name: '刷新', exact: true }).click()
+ await expect(message(page, text)).toBeVisible()
+ await expect(page.locator('.training-project-card')).toContainText(project.name)
+ await expect(message(page, text)).toBeHidden({ timeout: 8_000 })
+ await noPersistentError(page, text)
+ await shot(page, info, '02-content-old-data-retained')
+})
+
+test('系统用户:并行同错合并一条,重复搜索可再次提示', async ({ page, api }, info) => {
+ const text = '测试:用户目录服务暂不可用'
+ api.systemError = text
+ await page.goto('/system/users')
+ await expect(message(page, text)).toHaveCount(1)
+ await expect(message(page, text)).toBeVisible()
+ await expect.poll(() => api.requests.filter(entry => ['/api/auth/v1/users', '/api/auth/v1/users/stats', '/api/auth/v1/departments/tree', '/api/auth/v1/roles/all'].includes(entry.path)).length).toBe(4)
+ await expect(message(page, text)).toHaveCount(1)
+ await shot(page, info, '03-users-grouped-message')
+ await expect(message(page, text)).toBeHidden({ timeout: 8_000 })
+ await noPersistentError(page, text)
+ await page.getByRole('button', { name: '搜索', exact: true }).click()
+ await expect(message(page, text)).toHaveCount(1)
+ await expect(message(page, text)).toBeVisible()
+ await expect(message(page, text)).toBeHidden({ timeout: 8_000 })
+ await shot(page, info, '04-users-retry-without-banner')
+})
+
+test('训练编辑器:加载失败提示消失后保留重新加载按钮', async ({ page, api }, info) => {
+ const text = '测试:训练工程加载失败'
+ api.projectDetailError = text
+ await page.goto('/content/training-projects/message-training-901/legacy')
+ await expect(message(page, text)).toBeVisible()
+ await shot(page, info, '05-legacy-training-message')
+ await expect(message(page, text)).toBeHidden({ timeout: 8_000 })
+ await noPersistentError(page, text)
+ await expect(page.getByRole('button', { name: '重新加载', exact: true })).toBeVisible()
+ await shot(page, info, '06-legacy-training-retry')
+})
+
+test('实装预览:任务上下文错误用 Message,离线与教员模式持续可见', async ({ page, api }, info) => {
+ const text = '测试:实装任务上下文读取失败'
+ api.assignmentDetailError = text
+ await page.goto('/teaching/physical-training/preview/mock-physical')
+ await expect(message(page, text)).toBeVisible()
+ await expect(page.locator('.legacy-physical-page__warning')).toHaveText('离线识别回放')
+ await expect(page.locator('.legacy-physical-page__notice')).toContainText('教员预览态')
+ await shot(page, info, '07-physical-context-message')
+ await expect(message(page, text)).toBeHidden({ timeout: 8_000 })
+ await noPersistentError(page, text)
+ await expect(page.locator('.legacy-physical-page__warning')).toBeVisible()
+ await shot(page, info, '08-physical-mode-preserved')
+})
+
+test('新建训练:业务说明与必填校验保留,不发起创建请求', async ({ page }, info) => {
+ await page.goto('/content/training-projects')
+ await expect(page.locator('.training-project-card')).toBeVisible()
+ await page.getByRole('button', { name: '新建训练项目', exact: true }).click()
+ const dialog = page.getByRole('dialog')
+ await expect(dialog.locator('.el-alert--info')).toContainText('项目编码将在创建后由系统自动生成')
+ await dialog.getByRole('button', { name: '创建并编辑', exact: true }).click()
+ await expect(dialog.locator('.el-form-item__error').filter({ hasText: '请输入项目名称' })).toBeVisible()
+ await shot(page, info, '09-create-validation-preserved')
+})
+
+test('缓存列表:失活时晚到错误不弹出,返回不重放旧错误', async ({ page, api }, info) => {
+ const text = '测试:失活页面晚到错误'
+ api.holdProjects = true
+ await page.goto('/content/training-projects')
+ await expect.poll(() => api.heldProjects.length).toBeGreaterThan(0)
+ await page.evaluate(() => {
+ const state = window as unknown as { messageObservations: string[] }
+ state.messageObservations = []
+ new MutationObserver(() => document.querySelectorAll('.el-message').forEach(node => state.messageObservations.push(node.textContent || '')))
+ .observe(document.body, { childList: true, subtree: true })
+ })
+ await page.getByRole('button', { name: '系统管理', exact: true }).click()
+ await expect(page.locator('.users-table')).toBeVisible()
+ api.holdProjects = false
+ await Promise.all(api.heldProjects.splice(0).map(route => fail(route, text)))
+ await expect.poll(() => page.evaluate(async () => {
+ const modulePath = '/src/stores/content.ts'
+ const { useContentStore } = await import(modulePath)
+ return useContentStore().errorFor('projects:TRAINING')
+ })).toBe(text)
+ await expect(message(page, text)).toHaveCount(0)
+ await shot(page, info, '10-inactive-page-no-message')
+ await page.getByRole('tab', { name: '训练编排', exact: true }).click()
+ await expect(page.locator('.training-project-card')).toBeVisible()
+ await expect(message(page, text)).toHaveCount(0)
+ expect(await page.evaluate(value => (window as unknown as { messageObservations: string[] }).messageObservations.filter(entry => entry.includes(value)), text)).toEqual([])
+ await shot(page, info, '11-reactivated-no-stale-message')
+})
+
+test('HTTP 合同:真实 Axios 超时中文化,403/409 元数据保留,静默请求不自动弹窗', async ({ page }, info) => {
+ await page.goto('/content/training-projects')
+ await expect(page.locator('.training-project-card')).toBeVisible()
+ const errors = await page.evaluate(async () => {
+ const modulePath = '/src/api/http.ts'
+ const { requestData } = await import(modulePath)
+ const result = []
+ for (const endpoint of ['timeout', 'forbidden', 'conflict']) {
+ try { await requestData({ method: 'GET', url: `/__message_e2e/${endpoint}`, timeout: endpoint === 'timeout' ? 80 : 1_000 }) }
+ catch (cause) {
+ const error = cause as { message: string; code: string; status?: number; requestId: string }
+ result.push({ endpoint, message: error.message, code: error.code, status: error.status ?? null, requestId: error.requestId })
+ }
+ }
+ return result
+ })
+ expect(errors).toEqual([
+ { endpoint: 'timeout', message: '请求超时,请稍后重试', code: 'NETWORK_ERROR', status: null, requestId: '' },
+ { endpoint: 'forbidden', message: '无权访问测试数据', code: 'ACCESS_DENIED', status: 403, requestId: 'mock-403' },
+ { endpoint: 'conflict', message: '版本已变化,请重新加载', code: 'VERSION_CONFLICT', status: 409, requestId: 'mock-409' },
+ ])
+ await expect(page.locator('.el-message')).toHaveCount(0)
+ await info.attach('HTTP 合同证据', { body: JSON.stringify(errors, null, 2), contentType: 'application/json' })
+ const cancellation = await page.evaluate(async () => {
+ const httpPath = '/src/api/http.ts'
+ const messagePath = '/src/utils/errorMessage.ts'
+ const { requestData } = await import(httpPath)
+ const { showErrorMessage } = await import(messagePath)
+ const controller = new AbortController()
+ setTimeout(() => controller.abort(), 20)
+ try { await requestData({ method: 'GET', url: '/__message_e2e/timeout', signal: controller.signal, timeout: 1_000 }) }
+ catch (cause) {
+ showErrorMessage(cause)
+ const error = cause as { name: string; code: string }
+ return { name: error.name, code: error.code }
+ }
+ return null
+ })
+ expect(cancellation).toEqual({ name: 'CanceledError', code: 'ERR_CANCELED' })
+ await expect(page.locator('.el-message')).toHaveCount(0)
+ await shot(page, info, '12-http-silent-errors')
+ const businessError = await page.evaluate(async () => {
+ const httpPath = '/src/api/http.ts'
+ const messagePath = '/src/utils/errorMessage.ts'
+ const { requestData } = await import(httpPath)
+ const { showErrorMessage } = await import(messagePath)
+ try { await requestData({ method: 'GET', url: '/__message_e2e/business-text' }) }
+ catch (cause) {
+ showErrorMessage(cause)
+ const error = cause as { message: string; code: string; requestId: string; status: number }
+ return { message: error.message, code: error.code, requestId: error.requestId, status: error.status }
+ }
+ return null
+ })
+ expect(businessError).toEqual({ message: 'Network Error', code: 'ASSET_VALIDATION', requestId: 'mock-business', status: 400 })
+ await expect(message(page, 'Network Error')).toBeVisible()
+ await shot(page, info, '15-business-message-keeps-original-text')
+ await info.attach('取消请求与业务原文证据', { body: JSON.stringify({ cancellation, businessError }, null, 2), contentType: 'application/json' })
+})
+
+test('局域网教学列表:既有 ElMessage.error 使用全局关闭和自动消失配置', async ({ page, api }, info) => {
+ const text = '测试:局域网教学列表读取失败'
+ api.assignmentError = text
+ await page.goto('http://192.168.31.168:6180/teaching/physical-training')
+ await expect(message(page, text)).toBeVisible()
+ await expect(message(page, text).locator('.el-message__closeBtn')).toBeVisible()
+ await shot(page, info, '13-lan-teaching-message')
+ await expect(message(page, text)).toBeHidden({ timeout: 8_000 })
+ await noPersistentError(page, text)
+ await shot(page, info, '14-lan-teaching-after-dismiss')
+})
+
+test('模型桥接 DOM 合同:初次错误与晚写详情转为重试说明,ready 恢复交互', async ({ page }, info) => {
+ await page.route(url => url.pathname === '/__message_bridge_test', route => route.fulfill({
+ status: 200, contentType: 'text/html', body: `模型加载桥接 DOM 测试夹具模型加载桥接 DOM 合同
浏览器测试夹具,仅验证真实宿主桥接的错误文案、重试与交互恢复;不代表加载真实 GLB。
`,
+ }))
+ await page.goto('/__message_bridge_test')
+ await page.evaluate(async () => {
+ const state = window as unknown as { editorApp: { renderer: { domElement: Element | null } }; bridgeRetries: number }
+ state.editorApp = { renderer: { domElement: document.querySelector('canvas') } }
+ state.bridgeRetries = 0
+ const modulePath = '/src/features/editors/model/legacy-iframe-loading-scope.ts'
+ const { installLegacyModelIframeLoadingScope } = await import(modulePath)
+ installLegacyModelIframeLoadingScope(document, { onRetry: () => { state.bridgeRetries += 1 } })
+ document.querySelector('.editor-shell')!.dataset.modelApiState = 'error'
+ document.querySelector('#loading-progress')!.textContent = '测试:初次模型接口错误详情'
+ })
+ await expect(page.locator('#loading-progress')).toHaveText('请重新加载工程')
+ await expect(page.locator('body')).not.toContainText('测试:初次模型接口错误详情')
+ expect(await page.locator('.topbar').evaluate(element => (element as HTMLElement).inert)).toBe(true)
+ await page.locator('#loading-progress').evaluate(element => { element.textContent = '测试:晚写模型接口错误详情' })
+ await expect(page.locator('#loading-progress')).toHaveText('请重新加载工程')
+ await expect(page.locator('body')).not.toContainText('测试:晚写模型接口错误详情')
+ await page.getByRole('button', { name: '重新加载工程', exact: true }).click()
+ expect(await page.evaluate(() => (window as unknown as { bridgeRetries: number }).bridgeRetries)).toBe(1)
+ await shot(page, info, '16-model-bridge-neutral-retry')
+ await page.locator('.editor-shell').evaluate(element => { (element as HTMLElement).dataset.modelApiState = 'ready' })
+ await expect(page.locator('#loading-overlay')).toBeHidden()
+ expect(await page.locator('.topbar').evaluate(element => (element as HTMLElement).inert)).toBe(false)
+ await expect(page.locator('.editor-shell')).toHaveAttribute('data-model-project-ready', 'true')
+ await expect(page.locator('.editor-shell')).toHaveAttribute('data-model-interaction-blocked', 'false')
+ await shot(page, info, '17-model-bridge-ready')
+})
diff --git a/tests/auth.spec.ts b/tests/auth.spec.ts
index 9e0a298..c8e71ed 100644
--- a/tests/auth.spec.ts
+++ b/tests/auth.spec.ts
@@ -27,7 +27,7 @@ test('管理员可以登录并安全退出', async ({ page }, testInfo) => {
await page.locator('input[name="username"]').fill('__ute2e_missing_account__')
await page.locator('input[name="password"]').fill('Uae2e-Invalid-Only')
await page.getByRole('button', { name: /登录系统/ }).click()
- await expect(page.locator('.login-error')).toContainText('账号、密码或登录身份不匹配')
+ await expect(page.locator('.el-message--error')).toContainText('账号、密码或登录身份不匹配')
await login(page, adminCredentials())
await expect(page).toHaveURL(/\/dashboard(?:\?|$)/)
await captureScreenshot(page, testInfo, 'login-success')
diff --git a/tests/zentao-auth-859-861-864.spec.ts b/tests/zentao-auth-859-861-864.spec.ts
index 8a69282..a545c53 100644
--- a/tests/zentao-auth-859-861-864.spec.ts
+++ b/tests/zentao-auth-859-861-864.spec.ts
@@ -43,14 +43,19 @@ const fulfill = (route: Route, data: unknown, status = 200, message = '成功',
async function mockAuth(
page: Page,
- options: { me?: 'forced' | 'expired'; initialPassword401?: boolean } = {},
+ options: { me?: 'forced' | 'expired'; initialPassword401?: boolean; loginFailure?: boolean } = {},
) {
let refreshRequests = 0
let initialPasswordRequests = 0
+ let loginRequests = 0
await page.route('**/api/auth/v1/**', async (route) => {
const path = new URL(route.request().url()).pathname
if (path.endsWith('/auth/login-context')) return fulfill(route, loginContext)
if (path.endsWith('/auth/login-identities')) return fulfill(route, [])
+ if (path.endsWith('/auth/login') && route.request().method() === 'POST') {
+ loginRequests += 1
+ if (options.loginFailure) return fulfill(route, null, 401, '账号、密码或登录身份不匹配', 40102)
+ }
if (path.endsWith('/menus/navigation')) return fulfill(route, [])
if (path.endsWith('/auth/me')) {
if (options.me === 'expired') {
@@ -74,6 +79,7 @@ async function mockAuth(
return {
refreshRequests: () => refreshRequests,
initialPasswordRequests: () => initialPasswordRequests,
+ loginRequests: () => loginRequests,
}
}
@@ -130,7 +136,7 @@ test('864:首次改密期间账号停用后仅显示一次固定登录提示
await page.getByRole('button', { name: '确认修改并登录' }).click()
await expect(page).toHaveURL(/\/login\?redirect=/)
- await expect(page.locator('#login-error')).toHaveText('账号、密码或登录身份不匹配')
+ await expect(page.locator('.el-message--error')).toHaveText('账号、密码或登录身份不匹配')
await expect(page.getByText(/internal-user-id|refresh-token-internal-detail/)).toHaveCount(0)
expect(mocked.initialPasswordRequests()).toBe(1)
expect(mocked.refreshRequests()).toBe(0)
@@ -150,3 +156,25 @@ test('普通会话过期只跳转登录,不展示后端错误详情或停用
await expect(page.getByText(/内部会话详情|refresh-token-internal-detail|账号、密码或登录身份不匹配/)).toHaveCount(0)
expect(mocked.refreshRequests()).toBe(1)
})
+
+test('当前登录接口失败只显示临时 Message,自动消失且不留下登录错误条', async ({ page }, testInfo) => {
+ const mocked = await mockAuth(page, { loginFailure: true })
+ await page.goto('/login')
+ await page.locator('[data-role-code="admin"]').click()
+ await page.locator('input[name="username"]').fill('__ute2e_message_mock__')
+ await page.locator('input[name="password"]').fill('Invalid#Only8')
+ await page.getByRole('button', { name: '登录系统', exact: true }).click()
+
+ const message = page.locator('.el-message--error')
+ await expect(message).toHaveCount(1)
+ await expect(message).toHaveText('账号、密码或登录身份不匹配')
+ await expect(page.locator('#login-error')).toHaveText('')
+ expect(mocked.loginRequests()).toBe(1)
+ expect(mocked.refreshRequests()).toBe(0)
+ await captureScreenshot(page, testInfo, 'login-api-error-message')
+
+ await expect(message).toHaveCount(0, { timeout: 7_000 })
+ await expect(page.locator('#login-error')).toHaveText('')
+ await expect(page).toHaveURL(/\/login(?:\?|$)/)
+ await captureScreenshot(page, testInfo, 'login-api-error-message-dismissed')
+})
diff --git a/tools/build-api-error-message-report.mjs b/tools/build-api-error-message-report.mjs
new file mode 100644
index 0000000..6bf15cc
--- /dev/null
+++ b/tools/build-api-error-message-report.mjs
@@ -0,0 +1,88 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
+const root = path.join(repo, 'reports/api-error-messages-20260906')
+const result = JSON.parse(fs.readFileSync(path.join(root, 'results.json'), 'utf8'))
+const validationPath = path.join(root, 'validation.json')
+const validation = fs.existsSync(validationPath) ? JSON.parse(fs.readFileSync(validationPath, 'utf8')) : null
+const authenticationPath = path.join(root, 'authentication/summary.json')
+const authentication = fs.existsSync(authenticationPath) ? JSON.parse(fs.readFileSync(authenticationPath, 'utf8')) : null
+const escape = value => String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char])
+const decode = attachment => {
+ if (!attachment?.body) return null
+ try { return JSON.parse(Buffer.from(attachment.body, 'base64').toString('utf8')) } catch { return null }
+}
+const specs = []
+function visit(suite) {
+ specs.push(...(suite.specs || []))
+ for (const child of suite.suites || []) visit(child)
+}
+for (const suite of result.suites || []) visit(suite)
+const checks = specs.flatMap(spec => (spec.tests || []).map(test => {
+ const run = test.results?.at(-1) || {}
+ const screenshots = (run.attachments || []).filter(item => item.contentType === 'image/png' && fs.existsSync(path.join(root, 'screenshots', `${item.name}.png`)))
+ .map(item => ({ title: item.name, path: `screenshots/${item.name}.png` }))
+ const isolation = decode((run.attachments || []).find(item => item.name === 'API 隔离与浏览器错误'))
+ const http = decode((run.attachments || []).find(item => item.name === 'HTTP 合同证据'))
+ const httpBoundaries = decode((run.attachments || []).find(item => item.name === '取消请求与业务原文证据'))
+ return {
+ title: spec.title, status: run.status === 'passed' ? 'PASS' : String(run.status || 'NOT_RUN').toUpperCase(),
+ durationMs: run.duration || 0, screenshots, isolation, ...(http ? { http } : {}), ...(httpBoundaries ? { httpBoundaries } : {}),
+ }
+}))
+const passed = checks.filter(item => item.status === 'PASS').length
+const failed = checks.length - passed
+const screenshotCount = checks.reduce((sum, item) => sum + item.screenshots.length, 0)
+const requestCount = checks.reduce((sum, item) => sum + (item.isolation?.requests.length || 0), 0)
+const pageErrorCount = checks.reduce((sum, item) => sum + (item.isolation?.pageErrors.length || 0), 0)
+for (const check of checks) {
+ if (check.status === 'PASS' && !check.isolation) throw new Error(`缺少隔离证据:${check.title}`)
+}
+const authenticationShots = authentication?.checks?.flatMap(check => check.screenshots || []) || []
+for (const shot of authenticationShots) if (!fs.existsSync(path.join(root, shot.path))) throw new Error(`认证报告缺图:${shot.path}`)
+const stages = [
+ ['资料', '按本轮“接口错误使用 Message,不保留常驻错误横条”的要求检查。沿用已确认的教学业务边界:离线识别回放、观察模式、步骤反馈和表单校验仍需可见。'],
+ ['原型', '错误发生时顶部短暂提示;可以手动关闭,约 4.5 秒自动消失。同一错误合并展示;失败后仍可重新加载,已成功取得的数据不因后续刷新失败消失。'],
+ ['设计', '共享 Message 处理负责统一展示与缓存页面生命周期;请求层保留业务错误码、HTTP 状态和 requestId,静默请求不会自行弹窗。未取得数据与空结果保持区分。'],
+ ['开发', '代表性覆盖内容制作、系统用户与教学实施,包括训练编辑器加载失败的重试入口。接口异常详情仅在 Message 中展示;表单必填错误、项目编码说明和离线运行模式继续保留。'],
+ ['测试', '使用真实 Vue / Element Plus 页面、Edge 无头浏览器和浏览器级 API mock。覆盖本机与局域网入口。身份及接口数据均为合成测试数据;不登录真实账号、不写业务库,不将本轮测试称为真实服务端或设备联调。'],
+]
+const limitations = [
+ '这是接口失败展示的聚焦浏览器回归,未逐一遍历所有业务页面。',
+ '页面渲染、Element Plus Message、缓存路由和 Axios 超时均运行于真实浏览器;服务端响应由测试模拟。',
+ '模型 iframe 项为真实宿主桥接的 DOM 合同测试,使用空画布夹具,不代表完整 GLB 模型加载测试。',
+ '保留训练业务状态、表单校验与重试入口。未扩大教学流程、权限或接口合同。',
+]
+const evidence = {
+ title: '接口错误 Message 回归报告', status: failed ? 'FAILED' : authentication?.counts?.legacyBlocked ? 'PASS_WITH_LEGACY_BLOCKERS' : 'PASS',
+ generatedAt: new Date().toISOString(), startedAt: result.stats?.startTime,
+ durationMs: result.stats?.duration, passed, total: checks.length, screenshotCount, requestCount, pageErrorCount,
+ validation, authentication, stages: stages.map(([title, text]) => ({ title, text })), checks, limitations,
+ reproduction: 'pnpm exec playwright test -c api-error-messages.config.ts',
+}
+fs.writeFileSync(path.join(root, 'evidence.json'), JSON.stringify(evidence, null, 2) + '\n', 'utf8')
+
+const validationText = validation ? validation.commands.map(item => `${item.command}:${item.status}。${item.details}`).join('\n\n') : '构建与源代码审查由主任务单独记录;本报告统计浏览器回归结果。'
+const validationHtml = validation ? `构建与聚焦逻辑验证
${validation.commands.map(item => `${escape(item.status)} · ${escape(item.scope)}
${escape(item.command)}
${escape(item.details)}${item.log ? ` 日志` : ''}
`).join('')}` : ''
+const checkHtml = checks.map((check, index) => `${escape(check.status)}
${index + 1}. ${escape(check.title)}
${(check.durationMs / 1000).toFixed(1)} 秒${check.screenshots.map(shot => `
${escape(shot.title)}`).join('')}
`).join('')
+const authenticationHtml = authentication ? `认证附加回归(独立统计)
${escape(authentication.result)}
${escape(authentication.environment)}
${authentication.limitations.map(text => `${escape(text)}
`).join('')}${authentication.checks.map(check => `${escape(check.status)}
${escape(check.title)}
${escape(check.result)}
${check.rawStatus ? `Playwright 原始状态:${escape(check.rawStatus)}。
` : ''}${check.screenshots.map(shot => `
${escape(shot.title)}`).join('')}
`).join('')}` : ''
+fs.writeFileSync(path.join(root, 'index.html'), `
+接口错误 Message 回归报告
+接口错误 Message 回归报告
资料 → 原型 → 设计 → 开发 → 测试
${failed ? '主回归存在未通过项,详见下方结果。' : '主回归:代表性接口错误按统一 Message 展示;自动消失、重试与业务状态保留已通过浏览器验证。'}
${authentication ? `认证附加:${authentication.counts.passed} 项通过,${authentication.counts.legacyBlocked} 项旧改密入口受当前路由契约阻断;原始失败保留,未计为通过。
` : ''}主回归 ${passed} / ${checks.length} 通过主回归 ${screenshotCount} 张截图${requestCount} 次模拟 API 请求${pageErrorCount} 个未捕获页面异常
+审查与实施
${stages.map(([title, text]) => `
`).join('')}
浏览器验证(主回归)
${checkHtml}${authenticationHtml}${validationHtml}范围与复现
${limitations.map(text => `${escape(text)}
`).join('')}${escape(evidence.reproduction)}
报告重新生成:node tools/build-api-error-message-report.mjs
`, 'utf8')
+const md = [
+ '# 接口错误 Message 回归报告', '', `主回归:**${passed}/${checks.length} 通过**。${screenshotCount} 张截图,${requestCount} 次模拟 API 请求,${pageErrorCount} 个未捕获页面异常。`, '',
+ ...(authentication ? [`认证附加:${authentication.counts.passed} 项通过,${authentication.counts.legacyBlocked} 项旧改密入口受当前路由契约阻断。原始失败保留,未计为通过。总状态:${evidence.status}。`, ''] : []),
+ ...stages.flatMap(([title, text]) => [`## ${title}`, '', text, '']),
+ '## 浏览器结果', '',
+ ...checks.flatMap((check, index) => [`### ${index + 1}. ${check.title}`, '', `状态:${check.status}。耗时 ${(check.durationMs / 1000).toFixed(1)} 秒。`, '', ...check.screenshots.flatMap(shot => [``, ''])]),
+ ...(authentication ? ['## 认证附加回归(独立统计)', '', authentication.result, '', ...authentication.checks.flatMap(check => [`### ${check.title}`, '', `状态:${check.status}${check.rawStatus ? `;Playwright 原始状态:${check.rawStatus}` : ''}。${check.result}`, '', ...check.screenshots.flatMap(shot => [``, ''])]), ...authentication.limitations.map(text => `- ${text}`), '', '[认证汇总证据](authentication/summary.json)', ''] : []),
+ '## 范围与复现', '', ...limitations.map(text => `- ${text}`), '', validationText, '',
+ '```powershell', evidence.reproduction, 'node tools/build-api-error-message-report.mjs', '```', '',
+ `测试开始:${evidence.startedAt}。报告生成:${evidence.generatedAt}。`, '',
+]
+fs.writeFileSync(path.join(root, 'report.md'), md.join('\n'), 'utf8')
+console.log(JSON.stringify({ status: evidence.status, passed, total: checks.length, screenshotCount, requestCount, pageErrorCount }))
diff --git a/tools/guide-error-message.test.mjs b/tools/guide-error-message.test.mjs
new file mode 100644
index 0000000..7160aef
--- /dev/null
+++ b/tools/guide-error-message.test.mjs
@@ -0,0 +1,46 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+import { readFileSync } from 'node:fs'
+
+const source = readFileSync(new URL('../../unreal_tran_web/src/features/guide-legacy/demo-src/ofd-render-interactive.js', import.meta.url), 'utf8')
+const handlerSource = source.slice(source.indexOf(' const reportedLoadErrors = new WeakSet();'), source.indexOf(' const syncAnimationControls ='))
+
+function fixture(destroyed = false) {
+ const statuses = []
+ const messages = []
+ const report = new Function('destroyed', 'setStatus', 'onError', `${handlerSource}\nreturn reportLoadError`)(
+ destroyed,
+ (text, error) => statuses.push({ text, error }),
+ error => messages.push(error),
+ )
+ return { report, statuses, messages }
+}
+
+test('GLB load and ready rejection handlers report the same failure only once', () => {
+ const f = fixture()
+ const error = new Error('HTTP 503: asset service unavailable')
+ f.report(error)
+ f.report(error)
+ assert.deepEqual(f.messages, [error])
+ assert.ok(f.statuses.length > 0)
+ assert.ok(f.statuses.every(status => status.error && !status.text.includes(error.message)))
+ assert.match(f.statuses.at(-1).text, /重新加载/)
+})
+
+test('retrying an unavailable GLB still reports the new failed request', () => {
+ const f = fixture()
+ f.report(new Error('HTTP 503'))
+ f.report(new Error('HTTP 503'))
+ assert.equal(f.messages.length, 2)
+})
+
+test('cancelled and destroyed model viewers do not surface stale load errors', () => {
+ const f = fixture()
+ f.report(Object.assign(new Error('cancelled'), { name: 'AbortError' }))
+ assert.equal(f.messages.length, 0)
+ assert.equal(f.statuses.length, 0)
+ const destroyed = fixture(true)
+ destroyed.report(new Error('late HTTP 503'))
+ assert.equal(destroyed.messages.length, 0)
+ assert.equal(destroyed.statuses.length, 0)
+})
diff --git a/tools/scene-cover-api.test.mjs b/tools/scene-cover-api.test.mjs
index a879331..732a739 100644
--- a/tools/scene-cover-api.test.mjs
+++ b/tools/scene-cover-api.test.mjs
@@ -205,3 +205,63 @@ test('changes made while cover uploads remain dirty after the earlier snapshot s
assert.equal(editor.dirty, true)
assert.match(editor.messages.at(-1).message, /新修改仍待保存/)
})
+
+const publishMethod = editorSource.slice(editorSource.indexOf(' async publishScene() {'), editorSource.indexOf(' /*\n * 原实现只设置原生 hidden')).trim()
+const { publishScene } = new Function(`return ({ ${publishMethod} })`)()
+function publishingEditor(saveDocument) {
+ const editor = editorFixture(saveDocument)
+ editor.canPublish = true
+ editor.objects = []
+ editor.assetStore = { validateReferences: async () => ({ valid: true }) }
+ editor.verifyScene = () => [{ ok: true }]
+ editor.saveProject = saveProject
+ editor.publishCalls = 0
+ editor.context.publishDocument = async () => { editor.publishCalls += 1 }
+ return editor
+}
+
+const hostSource = readFileSync(new URL('../../unreal_tran_web/src/views/content/LegacyScenePageView.vue', import.meta.url), 'utf8')
+const leaveSource = hostSource.slice(hostSource.indexOf('async function confirmLeave()'), hostSource.indexOf('function resetProjectSession()'))
+const leaveCompiled = ts.transpileModule(leaveSource, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.None } }).outputText
+function leaveEditor(editor) {
+ return new Function('editorHandle', 'ElMessageBox', 'ElMessage', `${leaveCompiled}\nreturn confirmLeave()`)(
+ editor,
+ { confirm: async () => undefined },
+ { error: message => editor.toast(message, 'error') },
+ )
+}
+
+for (const status of [409, 503]) {
+ test(`publish after PUT ${status} stays blocked and reports the save failure only once`, async () => {
+ const editor = publishingEditor(async () => { throw Object.assign(new Error(`HTTP ${status}`), { status }) })
+ await publishScene.call(editor)
+ assert.equal(editor.publishCalls, 0)
+ assert.equal(editor.dirty, true)
+ assert.equal(editor.publishing, false)
+ assert.equal(editor.messages.length, 1)
+ assert.match(editor.messages[0].message, new RegExp(`HTTP ${status}`))
+ })
+}
+
+test('save-and-leave keeps the editor open with one failure message', async () => {
+ const editor = publishingEditor(async () => { throw new Error('version conflict') })
+ editor.dirty = true
+ assert.equal(await leaveEditor(editor), false)
+ assert.equal(editor.dirty, true)
+ assert.equal(editor.messages.length, 1)
+ assert.match(editor.messages[0].message, /version conflict/)
+})
+
+for (const action of ['publish', 'leave']) {
+ test(`${action} stays blocked when edits arrive during save, without claiming save failed`, async () => {
+ const editor = publishingEditor(async () => { editor.changeRevision += 1; return {} })
+ editor.dirty = true
+ if (action === 'publish') await publishScene.call(editor)
+ else assert.equal(await leaveEditor(editor), false)
+ assert.equal(editor.publishCalls, 0)
+ assert.equal(editor.dirty, true)
+ assert.equal(editor.messages.length, 1)
+ assert.match(editor.messages[0].message, /新修改仍待保存/)
+ assert.doesNotMatch(editor.messages[0].message, /保存失败|未保存成功/)
+ })
+}
diff --git a/tools/training-return-tabs.e2e.mjs b/tools/training-return-tabs.e2e.mjs
index f144508..5ad4cba 100644
--- a/tools/training-return-tabs.e2e.mjs
+++ b/tools/training-return-tabs.e2e.mjs
@@ -188,12 +188,13 @@ try {
currentTitle = '保存并离开失败时不导航、不删除编辑标签(浏览器模拟 503)'
await returnToList(page)
await page.getByRole('button', { name: '保存并离开', exact: true }).click()
- await expect(page.locator('.el-message-box').filter({ hasText: '保存失败' })).toBeVisible({ timeout: 45_000 })
+ const saveFailureMessage = page.locator('.el-message--error').filter({ hasText: '浏览器模拟保存失败' })
+ await expect(saveFailureMessage).toBeVisible({ timeout: 45_000 })
assert.equal(new URL(page.url()).pathname, editPath('161'))
assert.ok(has(await tabs(page), editPath('161')))
assert.ok(evidence.blockedWrites.some(write => write.method === 'PUT' && write.path.endsWith('/projects/161')))
passed(currentTitle, { tabs: await tabs(page), writes: evidence.blockedWrites, simulated: true }, [await screenshot(page, '06-save-failure-keeps-editor')])
- await page.getByRole('button', { name: '知道了', exact: true }).click()
+ await saveFailureMessage.locator('.el-message__closeBtn').click()
currentTitle = '明确放弃修改后返回并关闭当前编辑标签'
await returnToList(page)