agentworkshop 0.7.37 → 0.7.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.AgentWorkShop/prompts/aml-mission-default.md +20 -20
  2. package/.AgentWorkShop/prompts/aml-playbook.md +49 -49
  3. package/.AgentWorkShop/prompts/host-tools.json +1238 -1238
  4. package/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -10
  5. package/.output/.AgentWorkShop/prompts/aml-mission-default.md +20 -20
  6. package/.output/.AgentWorkShop/prompts/aml-playbook.md +49 -49
  7. package/.output/.AgentWorkShop/prompts/host-tools.json +1238 -1238
  8. package/.output/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -10
  9. package/.output/nitro.json +1 -1
  10. package/.output/public/_nuxt/{BG8sCcwx.js → CUDwzgLV.js} +4 -4
  11. package/.output/public/_nuxt/CmsOtOAA2.js +1 -0
  12. package/.output/public/_nuxt/builds/latest.json +1 -1
  13. package/.output/public/_nuxt/builds/meta/4457d63f-0866-4cea-b320-04a321f24795.json +1 -0
  14. package/.output/public/_nuxt/default.BXI_fZUi.css +1 -0
  15. package/.output/server/chunks/_/nitro.mjs +703 -704
  16. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  17. package/.output/server/chunks/build/{default-hseyOqBT.mjs → default-CZmZFpqH.mjs} +27 -42
  18. package/.output/server/chunks/build/{default-hseyOqBT.mjs.map → default-CZmZFpqH.mjs.map} +1 -1
  19. package/.output/server/chunks/routes/renderer.mjs +1 -1
  20. package/.output/server/chunks/virtual/entry.mjs +3 -1
  21. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  22. package/.output/server/package.json +1 -1
  23. package/README-zh.md +6 -6
  24. package/README.md +6 -6
  25. package/app/components/AppHeader.vue +0 -17
  26. package/app/components/AppSidebar.vue +1 -0
  27. package/docs/cli.en.md +2 -2
  28. package/docs/cli.md +2 -2
  29. package/docs/full-test-plan.md +5 -1
  30. package/docs/plugins.en.md +2 -1
  31. package/docs/plugins.md +2 -1
  32. package/docs/sdk.en.md +1 -1
  33. package/docs/sdk.md +1 -1
  34. package/i18n/locales/en.ts +1 -0
  35. package/i18n/locales/zh-CN.ts +1 -0
  36. package/package.json +1 -1
  37. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.continue.json +1 -0
  38. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.json +1 -0
  39. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/054bb4bc4e4217a2ab0b0c6b663747214a9af464f5568ea60e784704e340bb0f.source +153 -0
  40. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/0554197c3d2bac203a1f5c48a5d52495d1336db1bd7aa4f4c19875810363e684.source +153 -0
  41. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/05c203d12a954fffba942e1d626e3f5fec05b18a5aef2bac003b7884a4b77406.source +431 -0
  42. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/145a6e6ba811af35aed31e0cc3211f53706a14980fa069ee9e3c1591139f96f0.source +180 -0
  43. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/1ae0fb281f97899c025e368ca73f41349b01fb5f85b19e08d7ebccff49095ed5.source +112 -0
  44. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/20ee9967aa0541c516db7575bdd4a1a745939bc7016bd10d7635903113edb8ec.source +227 -0
  45. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/233751843aeba9d4c5263f275b8102de00443c06f286918cbd23e0b6729e01aa.source +131 -0
  46. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/263c2e05b425e5c322e60c6475279f963bab3a873eb521b21df7fec935a46597.source +160 -0
  47. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/2ac5eaac28eaaee2178c4b32014df8e732f9dd432a285a872f5b7f31c1395fa4.source +157 -0
  48. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/33a0db0d2d95d2c11d0720090458e5fba1425376f5ceb5b422d1d3471be3226b.source +348 -0
  49. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/359e474ae83db35e4615d3728de6bf694b64d4dce9b536730a3b6518f9c0494a.source +296 -0
  50. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/37a4c3c31c4b70f6b719c53d1e9c4fcba603f31adfdd11bb6ef9f0d094923c56.source +285 -0
  51. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/461feca1c14a695eb8b0f17a6f95fe97eeceabc0510faaea4ba6d25bde147d8d.source +217 -0
  52. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/4cfadbda8591f29d5e1e230dc000e701b756e850441b3900fb596a4d2f6e98c2.source +301 -0
  53. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/50ca2810cf0b248c96948994c0c404531bbad6e553166b6816b3b03bfa0f75d2.source +151 -0
  54. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/52d9e8cb591a9ae86e238c3620a610cd386717bee222c0f37b25b2ac7e523dc4.source +135 -0
  55. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/6c87daafa4c006560a954636a97333f6123153c9d34d96203f7ee3a14e556357.source +156 -0
  56. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/77bc47854b577900621cacf1697f81fa2bc005af129d37614a97a3441c08f99f.source +72 -0
  57. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/7bbb789d3a527817118cf8399ed1861d6a962ade10878fd07223435d333a7089.source +469 -0
  58. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/86b7c1daafc8b60ba9982dde5bf1bcc74265709483c176e71f23ec6fc267451f.source +259 -0
  59. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/8df66d62bd41984d0970e8236882080c5085ac937d07a29779463d6a03b37c76.source +97 -0
  60. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/9962f2d1b3a85137bd40cc835725600b39f89ee02a27c06b2e0402e1eafde115.source +277 -0
  61. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a1090ea066b792cb201739f4994e37bf7c12f52e805239a1eab87f18a537f266.source +171 -0
  62. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a5241091650134d0919897e086a606a3846c8b2b54f8b4c84a8696c145ac3e9e.source +425 -0
  63. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/af19fa4c6e34d5540028be4b17600161283af3a2a2dfc61a3fdc0834436a0c14.source +99 -0
  64. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3713d78e2b2f93905ff6be8d58a8d0a2f42dea322eb67c987fda22ff6a084d5.source +325 -0
  65. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3849943c3aff102d1af815631fc843a2a52c17646a4b04696ece323e483bbec.source +332 -0
  66. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/bac735a3803a6650b7f1eb8f587e6dc5c68ea055000c3c99a11789438b78e837.source +43 -0
  67. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d32c24d61eb8b33c73e5a8fc7e4dc7ef437aed238ee4371c2b00617e6104090a.source +154 -0
  68. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d5170dc9a9879fda04fdf91d8a110d4efb6787ee38dda596d4da899883d36a72.source +531 -0
  69. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/dc59bcd7bb397be63ba2f2c3fec23f3782dcb17d2d75a9981eeced761f33eb32.source +439 -0
  70. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/eb5ff8cf66b92aec7bf6726781d8df74faa2b030c3aaac17d6b00c1fc04c9b32.source +323 -0
  71. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f10615a6fae18436e70d1201b2d44fdae3174a3aa7bb4db97dea1b3ed291d721.source +433 -0
  72. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f36d3e271668bc1260c2823542d22dbf1d0f7041ff16b1ac175bce2256e582ca.source +280 -0
  73. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f94449380af09f31ac352152007146a75cc80bae8d6411ffe57b2c56f933b0ff.source +117 -0
  74. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/fe4dfd81178fd6b648335f6405d9ede71319aebde796008e50db21f8adf44c99.source +160 -0
  75. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/ffb4e3aa548cfdadabaa44ab599f9b6a3ca6288176a2cc7a4b2fc2a12b5fb848.source +101 -0
  76. package/scripts/_audit/detached-start.mjs +103 -0
  77. package/scripts/_audit/guard-upstream-liveness.mjs +104 -0
  78. package/scripts/_dbg-perms-e2e.mjs +10 -3
  79. package/scripts/_dbg-protocol-matrix.mjs +453 -441
  80. package/scripts/_dbg-render-regression.mjs +151 -130
  81. package/scripts/_dbg-town-agents-probe.mjs +60 -0
  82. package/scripts/_dbg-visual-acceptance.mjs +63 -0
  83. package/scripts/e2e-agent-team.mjs +5 -3
  84. package/scripts/test-plugin-hardening.mjs +32 -0
  85. package/scripts/three-system-e2e.mjs +6 -6
  86. package/server/plugins/dev-stability-guard.ts +40 -63
  87. package/server/plugins-builtin/rag-bridge/index.mjs +9 -2
  88. package/server/utils/stability-severity.ts +66 -0
  89. package/.output/public/_nuxt/DVsjtsvZ2.js +0 -1
  90. package/.output/public/_nuxt/builds/meta/a66fdef7-dcfd-453b-b1f6-543c7dfd98d5.json +0 -1
  91. package/.output/public/_nuxt/default.DE_vT238.css +0 -1
@@ -1,130 +1,151 @@
1
- /** 渲染优化功能回归:/daq 行渲染与交互、/daq/[id] 详情、/town 场景与模型预览、全局 smoke。
2
- * 用法:node scripts/_dbg-render-regression.mjs [base] [email] [pass] */
3
- import puppeteer from 'puppeteer-core'
4
- import { writeFileSync } from 'node:fs'
5
-
6
- const ROOT = process.argv[2] ?? 'http://127.0.0.1:3001'
7
- const EMAIL = process.argv[3] ?? 'perf-runner@awshop.io'
8
- const PASS = process.argv[4] ?? 'Perf@Run2026'
9
- const sleep = ms => new Promise(r => setTimeout(r, ms))
10
- const results = []
11
- const ok = (name, pass, detail = '') => {
12
- results.push({ name, pass, detail })
13
- console.log(`${pass ? '✔' : '✖'} ${name}${detail ? ' —— ' + detail : ''}`)
14
- }
15
-
16
- const login = await fetch(`${ROOT}/api/users/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: EMAIL, password: PASS }) }).then(r => r.json())
17
- if (!login?.data?.token) { console.error('LOGIN FAIL'); process.exit(1) }
18
- const browser = await puppeteer.launch({ executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe', headless: 'new', args: ['--no-sandbox', '--disable-gpu', '--window-size=1600,1000'] })
19
- const page = await browser.newPage()
20
- await page.setViewport({ width: 1600, height: 1000, deviceScaleFactor: 1 })
21
- await page.setCookie({ name: 'token', value: login.data.token, domain: new URL(ROOT).hostname, path: '/' })
22
- const pageErrors = []
23
- page.on('pageerror', e => pageErrors.push(String(e).slice(0, 200)))
24
-
25
- // ---------- 1. /daq 行渲染完整性 ----------
26
- await page.goto(`${ROOT}/daq`, { waitUntil: 'domcontentloaded', timeout: 60000 })
27
- await sleep(12000)
28
- const daqStruct = await page.evaluate(() => {
29
- const rows = document.querySelectorAll('.nodes-table tbody tr')
30
- const polylines = document.querySelectorAll('.nodes-table svg polyline')
31
- const limLines = document.querySelectorAll('.nodes-table svg line.lim')
32
- const pills = document.querySelectorAll('.nodes-table .st-pill')
33
- const valCells = [...document.querySelectorAll('.nodes-table .val')].slice(0, 5).map(c => c.textContent.trim())
34
- const toggles = document.querySelectorAll('.nodes-table .node-toggle')
35
- const links = document.querySelectorAll('.nodes-table .console-link')
36
- const lineSelects = document.querySelectorAll('.nodes-table .line-sel:not(.dev-add)')
37
- return { rows: rows.length, polylines: polylines.length, limLines: limLines.length, pills: pills.length, valCells, toggles: toggles.length, links: links.length, lineSelects: lineSelects.length }
38
- })
39
- ok('/daq 行数 227', daqStruct.rows === 227, `rows=${daqStruct.rows}`)
40
- ok('/daq 每行趋势折线', daqStruct.polylines === 227, `polylines=${daqStruct.polylines}`)
41
- ok('/daq 状态 pill 齐全', daqStruct.pills >= 227, `pills=${daqStruct.pills}`)
42
- ok('/daq 启停按钮齐全', daqStruct.toggles === 227, `toggles=${daqStruct.toggles}`)
43
- ok('/daq 控制台链接齐全', daqStruct.links === 227, `links=${daqStruct.links}`)
44
- ok('/daq 产线下拉齐全', daqStruct.lineSelects === 227, `selects=${daqStruct.lineSelects}`)
45
- ok('/daq 值列有数据', daqStruct.valCells.some(v => /\d/.test(v)), daqStruct.valCells[0])
46
-
47
- // ---------- 2. WS 实时收敛:值列随读数帧变化(优化后合批仍须驱动行更新) ----------
48
- const valChanges = await page.evaluate(() => new Promise((resolve) => {
49
- const cells = [...document.querySelectorAll('.nodes-table .val')]
50
- const texts = cells.map(c => c.textContent.trim())
51
- setTimeout(() => {
52
- const now = cells.map(c => c.textContent.trim())
53
- let changed = 0
54
- for (let i = 0; i < texts.length; i++) if (now[i] !== texts[i]) changed++
55
- resolve(changed)
56
- }, 8000)
57
- }))
58
- ok('/daq WS 读数驱动行更新(8s 内 ≥5 行变化)', valChanges >= 5, `changedRows=${valChanges}`)
59
-
60
- // ---------- 3. 筛选交互(行组件随筛选增减) ----------
61
- await page.type('.nodes-search input, input[placeholder]', '压力', { delay: 30 }).catch(() => {})
62
- await sleep(1500)
63
- const searchFiltered = await page.evaluate(() => document.querySelectorAll('.nodes-table tbody tr').length)
64
- ok('/daq 搜索筛选生效', searchFiltered > 0 && searchFiltered < 227, `rows=${searchFiltered}`)
65
- await page.evaluate(() => { location.hash = '' })
66
- // 清空搜索(全选删除)
67
- await page.keyboard.down('Control'); await page.keyboard.press('a'); await page.keyboard.up('Control'); await page.keyboard.press('Backspace')
68
- await sleep(1200)
69
- const afterClear = await page.evaluate(() => document.querySelectorAll('.nodes-table tbody tr').length)
70
- ok('/daq 清空搜索恢复全量', afterClear === 227, `rows=${afterClear}`)
71
-
72
- // ---------- 4. /daq/[id] 详情页(由行内控制台链接进入) ----------
73
- const detailHref = await page.evaluate(() => document.querySelector('.nodes-table .console-link')?.getAttribute('href'))
74
- await page.goto(`${ROOT}${detailHref}`, { waitUntil: 'domcontentloaded', timeout: 60000 })
75
- await sleep(6000)
76
- const detail = await page.evaluate(() => ({
77
- bigVal: document.querySelector('.big-val')?.textContent?.trim() ?? '',
78
- liveCanvas: !!document.querySelector('.live-canvas'),
79
- histCanvas: !!document.querySelector('.hist-canvas'),
80
- facts: document.querySelectorAll('.facts div').length,
81
- }))
82
- ok('/daq/[id] 大值卡渲染', /\d/.test(detail.bigVal), detail.bigVal.slice(0, 30))
83
- ok('/daq/[id] live 趋势画布存在', detail.liveCanvas)
84
- ok('/daq/[id] 历史画布存在', detail.histCanvas)
85
- ok('/daq/[id] 参数事实表渲染', detail.facts >= 5, `facts=${detail.facts}`)
86
-
87
- // ---------- 5. /town 场景 + 仪表化 + 模型预览 ----------
88
- await page.goto(`${ROOT}/town`, { waitUntil: 'domcontentloaded', timeout: 60000 })
89
- await sleep(18000)
90
- const town1 = await page.evaluate(() => ({
91
- canvas: !!document.querySelector('#town-host canvas'),
92
- stats: window.__townStats ? { ...window.__townStats } : null,
93
- }))
94
- ok('/town 3D 画布挂载', town1.canvas)
95
- ok('/town 仪表化 __townStats 存在', !!town1.stats)
96
- ok('/town 场景实体(Agent/设备)', !!town1.stats && town1.stats.agents >= 12 && town1.stats.devices >= 30, `agents=${town1.stats?.agents} devices=${town1.stats?.devices}`)
97
- ok('/town 帧率 > 0(墙钟)', !!town1.stats && town1.stats.fps > 0, `fps=${town1.stats?.fps} rafHz=${town1.stats?.rafHz}`)
98
- ok('/town 60fps 预算生效', !!town1.stats && town1.stats.frameBudgetMs === 16.67, `frameBudgetMs=${town1.stats?.frameBudgetMs}`)
99
- const town2 = await page.evaluate(() => new Promise((resolve) => {
100
- const s0 = JSON.stringify(window.__townStats)
101
- setTimeout(() => resolve({ s0, s1: JSON.stringify(window.__townStats) }), 3000)
102
- }))
103
- ok('/town 仪表化持续更新', town2.s0 !== town2.s1)
104
-
105
- // 打开模型库抽屉(若存在入口按钮)→ 预览卡渲染不崩
106
- const libOpened = await page.evaluate(() => {
107
- const btn = [...document.querySelectorAll('button, .hud-btn, [class*="lib"]')].find(b => /模型/.test(b.textContent ?? ''))
108
- if (btn) { btn.click(); return true }
109
- return false
110
- })
111
- if (libOpened) {
112
- await sleep(4000)
113
- const previews = await page.evaluate(() => document.querySelectorAll('.model-preview-3d canvas').length)
114
- ok('/town 模型库打开且预览卡有画布', previews > 0, `previews=${previews}`)
115
- }
116
- await page.screenshot({ path: '.e2e-shots/regression-town.png' })
117
-
118
- // ---------- 6. 全局页面 smoke(200 + 无 pageerror) ----------
119
- for (const path of ['/workshop', '/dcw', '/logs', '/monitor', '/settings', '/users', '/permissions']) {
120
- const resp = await page.goto(`${ROOT}${path}`, { waitUntil: 'domcontentloaded', timeout: 60000 }).then(r => r.status()).catch(() => 0)
121
- await sleep(1500)
122
- ok(`smoke ${path}`, resp === 200, `status=${resp}`)
123
- }
124
-
125
- ok('全程无 pageerror', pageErrors.length === 0, pageErrors.slice(0, 3).join(' | '))
126
- const failed = results.filter(r => !r.pass)
127
- writeFileSync('.e2e-shots/render-regression.json', JSON.stringify({ at: new Date().toISOString(), base: ROOT, passed: results.length - failed.length, failed: failed.length, results }, null, 2))
128
- console.log(`\n==== ${results.length - failed.length}/${results.length} PASS ====${failed.length ? ' FAILED: ' + failed.map(f => f.name).join(', ') : ''}`)
129
- await browser.close()
130
- process.exit(failed.length ? 1 : 0)
1
+ /** 渲染优化功能回归:/daq 行渲染与交互、/daq/[id] 详情、/town 场景与模型预览、全局 smoke。
2
+ * 用法:node scripts/_dbg-render-regression.mjs [base] [email] [pass] */
3
+ import puppeteer from 'puppeteer-core'
4
+ import { writeFileSync } from 'node:fs'
5
+
6
+ const ROOT = process.argv[2] ?? 'http://127.0.0.1:3001'
7
+ const EMAIL = process.argv[3] ?? 'perf-runner@awshop.io'
8
+ const PASS = process.argv[4] ?? 'Perf@Run2026'
9
+ const sleep = ms => new Promise(r => setTimeout(r, ms))
10
+ const results = []
11
+ const ok = (name, pass, detail = '') => {
12
+ results.push({ name, pass, detail })
13
+ console.log(`${pass ? '✔' : '✖'} ${name}${detail ? ' —— ' + detail : ''}`)
14
+ }
15
+
16
+ const login = await fetch(`${ROOT}/api/users/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: EMAIL, password: PASS }) }).then(r => r.json())
17
+ if (!login?.data?.token) { console.error('LOGIN FAIL'); process.exit(1) }
18
+ const browser = await puppeteer.launch({ executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe', headless: 'new', args: ['--no-sandbox', '--disable-gpu', '--window-size=1600,1000'] })
19
+ const page = await browser.newPage()
20
+ await page.setViewport({ width: 1600, height: 1000, deviceScaleFactor: 1 })
21
+ await page.setCookie({ name: 'token', value: login.data.token, domain: new URL(ROOT).hostname, path: '/' })
22
+ const pageErrors = []
23
+ page.on('pageerror', e => pageErrors.push(String(e).slice(0, 200)))
24
+
25
+ // 动态基线:节点数从 API 取(共享实例会增长,硬编码计数必然漂移)
26
+ const AUTH = { authorization: `Bearer ${login.data.token}` }
27
+ const daqCount = (await fetch(`${ROOT}/api/workshop/daq`, { headers: AUTH }).then(r => r.json())).data?.nodes?.length ?? 0
28
+ ok('/daq 基线计数(daq API)', daqCount > 0, `nodes=${daqCount}`)
29
+
30
+ // ---------- 1. /daq 行渲染完整性 ----------
31
+ await page.goto(`${ROOT}/daq`, { waitUntil: 'domcontentloaded', timeout: 60000 })
32
+ await sleep(12000)
33
+ const daqStruct = await page.evaluate(() => {
34
+ const rows = document.querySelectorAll('.nodes-table tbody tr')
35
+ const polylines = document.querySelectorAll('.nodes-table svg polyline')
36
+ const limLines = document.querySelectorAll('.nodes-table svg line.lim')
37
+ const pills = document.querySelectorAll('.nodes-table .st-pill')
38
+ const valCells = [...document.querySelectorAll('.nodes-table .val')].slice(0, 5).map(c => c.textContent.trim())
39
+ const toggles = document.querySelectorAll('.nodes-table .node-toggle')
40
+ const links = document.querySelectorAll('.nodes-table .console-link')
41
+ const lineSelects = document.querySelectorAll('.nodes-table .line-sel:not(.dev-add)')
42
+ return { rows: rows.length, polylines: polylines.length, limLines: limLines.length, pills: pills.length, valCells, toggles: toggles.length, links: links.length, lineSelects: lineSelects.length }
43
+ })
44
+ ok('/daq 行数=API 节点数', daqStruct.rows === daqCount, `rows=${daqStruct.rows} expect=${daqCount}`)
45
+ ok('/daq 趋势折线有渲染(有数节点)', daqStruct.polylines >= Math.min(10, daqCount) && daqStruct.polylines <= daqCount, `polylines=${daqStruct.polylines}/${daqCount}`)
46
+ ok('/daq 状态 pill 齐全', daqStruct.pills >= daqCount, `pills=${daqStruct.pills}`)
47
+ ok('/daq 启停按钮齐全', daqStruct.toggles === daqCount, `toggles=${daqStruct.toggles}`)
48
+ ok('/daq 控制台链接齐全', daqStruct.links === daqCount, `links=${daqStruct.links}`)
49
+ ok('/daq 产线下拉齐全', daqStruct.lineSelects === daqCount, `selects=${daqStruct.lineSelects}`)
50
+ ok('/daq 值列有数据', daqStruct.valCells.some(v => /\d/.test(v)), daqStruct.valCells[0])
51
+
52
+ // ---------- 2. WS 实时收敛:值列随读数帧变化(优化后合批仍须驱动行更新) ----------
53
+ const valChanges = await page.evaluate(() => new Promise((resolve) => {
54
+ const cells = [...document.querySelectorAll('.nodes-table .val')]
55
+ const texts = cells.map(c => c.textContent.trim())
56
+ setTimeout(() => {
57
+ const now = cells.map(c => c.textContent.trim())
58
+ let changed = 0
59
+ for (let i = 0; i < texts.length; i++) if (now[i] !== texts[i]) changed++
60
+ resolve(changed)
61
+ }, 8000)
62
+ }))
63
+ ok('/daq WS 读数驱动行更新(8s 内 ≥5 行变化)', valChanges >= 5, `changedRows=${valChanges}`)
64
+
65
+ // ---------- 3. 筛选交互(行组件随筛选增减) ----------
66
+ await page.type('.nodes-search input, input[placeholder]', '压力', { delay: 30 }).catch(() => {})
67
+ await sleep(1500)
68
+ const searchFiltered = await page.evaluate(() => document.querySelectorAll('.nodes-table tbody tr').length)
69
+ ok('/daq 搜索筛选生效', searchFiltered > 0 && searchFiltered < daqCount, `rows=${searchFiltered}`)
70
+ await page.evaluate(() => { location.hash = '' })
71
+ // 清空搜索(全选删除)
72
+ await page.keyboard.down('Control'); await page.keyboard.press('a'); await page.keyboard.up('Control'); await page.keyboard.press('Backspace')
73
+ await sleep(1200)
74
+ const afterClear = await page.evaluate(() => document.querySelectorAll('.nodes-table tbody tr').length)
75
+ ok('/daq 清空搜索恢复全量', afterClear === daqCount, `rows=${afterClear} expect=${daqCount}`)
76
+
77
+ // ---------- 4. /daq/[id] 详情页(由行内控制台链接进入) ----------
78
+ const detailHref = await page.evaluate(() => document.querySelector('.nodes-table .console-link')?.getAttribute('href'))
79
+ await page.goto(`${ROOT}${detailHref}`, { waitUntil: 'domcontentloaded', timeout: 60000 })
80
+ await sleep(6000)
81
+ const detail = await page.evaluate(() => ({
82
+ bigVal: document.querySelector('.big-val')?.textContent?.trim() ?? '',
83
+ liveCanvas: !!document.querySelector('.live-canvas'),
84
+ histCanvas: !!document.querySelector('.hist-canvas'),
85
+ facts: document.querySelectorAll('.facts div').length,
86
+ }))
87
+ ok('/daq/[id] 大值卡渲染', /\d/.test(detail.bigVal), detail.bigVal.slice(0, 30))
88
+ ok('/daq/[id] live 趋势画布存在', detail.liveCanvas)
89
+ ok('/daq/[id] 历史画布存在', detail.histCanvas)
90
+ ok('/daq/[id] 参数事实表渲染', detail.facts >= 5, `facts=${detail.facts}`)
91
+
92
+ // ---------- 5. /town 场景 + 仪表化 + 模型预览 ----------
93
+ // 夹具自愈:hydrate 只呈现「已放置」频道 —— 共享实例的布局/挂载清单随各轮测试漂移,
94
+ // 零交集时 agents=0 是数据态而非代码回归;故断言前确保第一个挂载频道已被放置。
95
+ {
96
+ const wsList = (await fetch(`${ROOT}/api/workshop/workspaces`, { headers: AUTH }).then(r => r.json())).data ?? []
97
+ const firstCid = wsList.flatMap(w => w.channelIds ?? [])[0]
98
+ const layoutsRes = await fetch(`${ROOT}/api/workshop/scene/layouts`, { headers: AUTH }).then(r => r.json())
99
+ const laidIds = new Set((layoutsRes.data?.layouts ?? []).map(l => l.channelId))
100
+ if (firstCid && !laidIds.has(firstCid)) {
101
+ const put = await fetch(`${ROOT}/api/workshop/scene/layouts/${firstCid}`, {
102
+ method: 'PUT', headers: { ...AUTH, 'content-type': 'application/json' },
103
+ body: JSON.stringify({ x: 1150, z: 1500, radiusX: 320, radiusZ: 210, shape: 'ellipse' }),
104
+ }).then(r => r.json())
105
+ console.log(` [fixture] 放置频道 ${firstCid.slice(0, 8)} → ${put.code === 0 ? 'ok' : put.message}`)
106
+ await sleep(1000)
107
+ }
108
+ }
109
+ await page.goto(`${ROOT}/town`, { waitUntil: 'domcontentloaded', timeout: 60000 })
110
+ await sleep(18000)
111
+ const town1 = await page.evaluate(() => ({
112
+ canvas: !!document.querySelector('#town-host canvas'),
113
+ stats: window.__townStats ? { ...window.__townStats } : null,
114
+ }))
115
+ ok('/town 3D 画布挂载', town1.canvas)
116
+ ok('/town 仪表化 __townStats 存在', !!town1.stats)
117
+ ok('/town 场景实体(Agent/设备)', !!town1.stats && town1.stats.agents >= 1 && town1.stats.devices >= 5, `agents=${town1.stats?.agents} devices=${town1.stats?.devices}`)
118
+ ok('/town 帧率 > 0(墙钟)', !!town1.stats && town1.stats.fps > 0, `fps=${town1.stats?.fps} rafHz=${town1.stats?.rafHz}`)
119
+ ok('/town 60fps 预算生效', !!town1.stats && town1.stats.frameBudgetMs === 16.67, `frameBudgetMs=${town1.stats?.frameBudgetMs}`)
120
+ const town2 = await page.evaluate(() => new Promise((resolve) => {
121
+ const s0 = JSON.stringify(window.__townStats)
122
+ setTimeout(() => resolve({ s0, s1: JSON.stringify(window.__townStats) }), 3000)
123
+ }))
124
+ ok('/town 仪表化持续更新', town2.s0 !== town2.s1)
125
+
126
+ // 打开模型库抽屉(若存在入口按钮)→ 预览卡渲染不崩
127
+ const libOpened = await page.evaluate(() => {
128
+ const btn = [...document.querySelectorAll('button, .hud-btn, [class*="lib"]')].find(b => /模型/.test(b.textContent ?? ''))
129
+ if (btn) { btn.click(); return true }
130
+ return false
131
+ })
132
+ if (libOpened) {
133
+ await sleep(4000)
134
+ const previews = await page.evaluate(() => document.querySelectorAll('.model-preview-3d canvas').length)
135
+ ok('/town 模型库打开且预览卡有画布', previews > 0, `previews=${previews}`)
136
+ }
137
+ await page.screenshot({ path: '.e2e-shots/regression-town.png' })
138
+
139
+ // ---------- 6. 全局页面 smoke(200 + 无 pageerror) ----------
140
+ for (const path of ['/workshop', '/dcw', '/logs', '/monitor', '/settings', '/users', '/permissions']) {
141
+ const resp = await page.goto(`${ROOT}${path}`, { waitUntil: 'domcontentloaded', timeout: 60000 }).then(r => r.status()).catch(() => 0)
142
+ await sleep(1500)
143
+ ok(`smoke ${path}`, resp === 200, `status=${resp}`)
144
+ }
145
+
146
+ ok('全程无 pageerror', pageErrors.length === 0, pageErrors.slice(0, 3).join(' | '))
147
+ const failed = results.filter(r => !r.pass)
148
+ writeFileSync('.e2e-shots/render-regression.json', JSON.stringify({ at: new Date().toISOString(), base: ROOT, passed: results.length - failed.length, failed: failed.length, results }, null, 2))
149
+ console.log(`\n==== ${results.length - failed.length}/${results.length} PASS ====${failed.length ? ' FAILED: ' + failed.map(f => f.name).join(', ') : ''}`)
150
+ await browser.close()
151
+ process.exit(failed.length ? 1 : 0)
@@ -0,0 +1,60 @@
1
+ /** 诊断探针:/town 场景 agents=0 之谜 —— 注入 WS 钩子记录 channel.snapshot,转储 __townStats。 */
2
+ import puppeteer from 'puppeteer-core'
3
+
4
+ const ROOT = process.argv[2] ?? 'http://127.0.0.1:3001'
5
+ const EMAIL = process.argv[3] ?? 'admin@awshop.local'
6
+ const PASS = process.argv[4] ?? 'admin123'
7
+ const sleep = ms => new Promise(r => setTimeout(r, ms))
8
+
9
+ const login = await fetch(`${ROOT}/api/users/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: EMAIL, password: PASS }) }).then(r => r.json())
10
+ if (!login?.data?.token) { console.error('LOGIN FAIL'); process.exit(1) }
11
+ const browser = await puppeteer.launch({ executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe', headless: 'new', args: ['--no-sandbox', '--disable-gpu', '--window-size=1600,1000'] })
12
+ const page = await browser.newPage()
13
+ await page.setViewport({ width: 1600, height: 1000, deviceScaleFactor: 1 })
14
+ await page.setCookie({ name: 'token', value: login.data.token, domain: new URL(ROOT).hostname, path: '/' })
15
+ await page.evaluateOnNewDocument(() => {
16
+ window.__wslog = []
17
+ const OrigWS = window.WebSocket
18
+ window.WebSocket = function (url, protocols) {
19
+ window.__wslog.push({ ev: 'open-req', url: String(url) })
20
+ const ws = protocols !== undefined ? new OrigWS(url, protocols) : new OrigWS(url)
21
+ ws.addEventListener('open', () => window.__wslog.push({ ev: 'open', url: String(url) }))
22
+ ws.addEventListener('close', (e) => window.__wslog.push({ ev: 'close', code: e.code, reason: e.reason }))
23
+ ws.addEventListener('error', () => window.__wslog.push({ ev: 'error', url: String(url) }))
24
+ ws.addEventListener('message', (ev) => {
25
+ const raw = typeof ev.data === 'string' ? ev.data : ''
26
+ let entry = { ev: 'msg', bytes: ev.data?.size ?? raw.length, kind: ev.data?.constructor?.name }
27
+ try {
28
+ const d = JSON.parse(raw)
29
+ entry.t = d?.type ?? d?.event ?? '?'
30
+ if (String(entry.t).includes('snapshot')) {
31
+ const ag = d?.data?.agents ?? d?.payload?.agents
32
+ entry.agents = Array.isArray(ag) ? ag.length : typeof ag
33
+ entry.channelId = d?.data?.channelId ?? d?.channelId
34
+ }
35
+ }
36
+ catch { entry.parse = 'fail' }
37
+ window.__wslog.push(entry)
38
+ })
39
+ const origSend = ws.send.bind(ws)
40
+ ws.send = (d) => { window.__wslog.push({ ev: 'send', head: String(d).slice(0, 120) }); return origSend(d) }
41
+ return ws
42
+ }
43
+ window.WebSocket.prototype = OrigWS.prototype
44
+ window.WebSocket.CONNECTING = OrigWS.CONNECTING
45
+ window.WebSocket.OPEN = OrigWS.OPEN
46
+ window.WebSocket.CLOSING = OrigWS.CLOSING
47
+ window.WebSocket.CLOSED = OrigWS.CLOSED
48
+ })
49
+ page.on('pageerror', e => console.log('PAGEERROR:', String(e).slice(0, 160)))
50
+
51
+ await page.goto(`${ROOT}/town`, { waitUntil: 'domcontentloaded', timeout: 60000 })
52
+ await sleep(15000)
53
+ const dump = await page.evaluate(() => ({
54
+ stats: window.__townStats ?? null,
55
+ wslog: (window.__wslog ?? []).slice(0, 30),
56
+ empty: !!document.querySelector('[data-hud="town-empty"]'),
57
+ loading: !!document.querySelector('[data-hud="town-loading"]'),
58
+ }))
59
+ console.log(JSON.stringify(dump, null, 1))
60
+ await browser.close()
@@ -0,0 +1,63 @@
1
+ /** P7 目视验收截图:关键页面逐页截图到 .e2e-shots/p7-*.png,并汇总每页 console error。 */
2
+ import puppeteer from 'puppeteer-core'
3
+ import { mkdirSync } from 'node:fs'
4
+
5
+ const ROOT = process.argv[2] ?? 'http://127.0.0.1:3001'
6
+ const EMAIL = process.argv[3] ?? 'admin@awshop.local'
7
+ const PASS = process.argv[4] ?? 'admin123'
8
+ const OUT = '.e2e-shots'
9
+ mkdirSync(OUT, { recursive: true })
10
+
11
+ const login = await fetch(`${ROOT}/api/users/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ email: EMAIL, password: PASS }) }).then(r => r.json())
12
+ if (!login?.data?.token) { console.error('LOGIN FAIL'); process.exit(1) }
13
+
14
+ const browser = await puppeteer.launch({ executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe', headless: 'new', args: ['--no-sandbox', '--disable-gpu', '--window-size=1600,1000'] })
15
+ const page = await browser.newPage()
16
+ await page.setViewport({ width: 1600, height: 1000, deviceScaleFactor: 1 })
17
+ await page.setCookie({ name: 'token', value: login.data.token, domain: new URL(ROOT).hostname, path: '/' })
18
+
19
+ const pages = [
20
+ ['workshop', '/workshop', 9000],
21
+ ['daq', '/daq', 12000],
22
+ ['dcw', '/dcw', 9000],
23
+ ['town', '/town', 20000],
24
+ ['settings', '/settings', 9000],
25
+ ['monitor', '/monitor', 9000],
26
+ ['aml', '/aml', 9000],
27
+ ['logs', '/logs', 9000],
28
+ ['users', '/users', 9000],
29
+ ]
30
+ const summary = []
31
+ for (const [name, path, wait] of pages) {
32
+ const errors = []
33
+ const onConsole = (m) => { if (m.type() === 'error') errors.push(m.text().slice(0, 120)) }
34
+ page.on('console', onConsole)
35
+ try {
36
+ await page.goto(`${ROOT}${path}`, { waitUntil: 'domcontentloaded', timeout: 60000 })
37
+ await new Promise(r => setTimeout(r, wait))
38
+ const file = `${OUT}/p7-${name}.png`
39
+ await page.screenshot({ path: file })
40
+ summary.push({ name, ok: true, errors })
41
+ console.log(`✓ ${name} → ${file}${errors.length ? ` (console errors: ${errors.length})` : ''}`)
42
+ } catch (e) {
43
+ summary.push({ name, ok: false, errors: [String(e).slice(0, 120)] })
44
+ console.log(`✖ ${name}: ${String(e).slice(0, 120)}`)
45
+ }
46
+ page.off('console', onConsole)
47
+ }
48
+ // daq 详情页(从列表第一个 console-link 进入)
49
+ try {
50
+ await page.goto(`${ROOT}/daq`, { waitUntil: 'domcontentloaded', timeout: 60000 })
51
+ await new Promise(r => setTimeout(r, 10000))
52
+ const href = await page.evaluate(() => document.querySelector('.nodes-table .console-link')?.getAttribute('href'))
53
+ if (href) {
54
+ await page.goto(`${ROOT}${href}`, { waitUntil: 'domcontentloaded', timeout: 60000 })
55
+ await new Promise(r => setTimeout(r, 8000))
56
+ await page.screenshot({ path: `${OUT}/p7-daq-detail.png` })
57
+ console.log(`✓ daq-detail → ${OUT}/p7-daq-detail.png`)
58
+ }
59
+ } catch (e) { console.log(`✖ daq-detail: ${String(e).slice(0, 100)}`) }
60
+ await browser.close()
61
+ const bad = summary.filter(s => !s.ok || s.errors.length)
62
+ console.log(`\n页面数=${summary.length} 有问题页=${bad.length}`)
63
+ for (const b of bad) console.log(' -', b.name, b.errors.slice(0, 2).join(' | '))
@@ -7,7 +7,9 @@
7
7
  * 5. 成员移除 / team 更新 / 删除
8
8
  * 运行: node scripts/e2e-agent-team.mjs [--base http://127.0.0.1:3000]
9
9
  */
10
- const BASE = (process.argv.find(a => a.startsWith('--base='))?.slice(7) ?? 'http://127.0.0.1:3000') + '/api/workshop'
10
+ const BASE = (process.argv.find(a => a.startsWith('--base='))?.slice(7) ?? 'http://127.0.0.1:3000') + '/api/workshop'
11
+ // 注册走平台 origin 的 /api/users/register(不在 /api/workshop 下;注册现要求 email+password)
12
+ const ORIGIN = BASE.slice(0, BASE.indexOf('/api'))
11
13
 
12
14
  let pass = 0
13
15
  let fail = 0
@@ -21,9 +23,9 @@ function check(name, ok, detail = '') {
21
23
  }
22
24
 
23
25
  // 用户级隔离:注册测试用户;管理面 API 全程携带用户 token
24
- const __user = await fetch(BASE + '/users/register', {
26
+ const __user = await fetch(ORIGIN + '/api/users/register', {
25
27
  method: 'POST', headers: { 'content-type': 'application/json' },
26
- body: JSON.stringify({ name: 'e2e-' + Math.random().toString(36).slice(2, 10) }),
28
+ body: JSON.stringify({ name: 'e2e-' + Math.random().toString(36).slice(2, 10), email: `e2e-team-${Date.now()}@test.local`, password: 'Passw0rd!123' }),
27
29
  }).then(r => r.json()).catch(() => null)
28
30
  const __userToken = __user?.data?.token
29
31
  if (!__userToken) {
@@ -46,5 +46,37 @@ console.log('\n━━━ 2. 路由表按插件卸下(P0-1 回滚所需) ━━
46
46
  check('unregisterPlugin 幂等(再调返回 0)', t.unregisterPlugin('alpha') === 0)
47
47
  }
48
48
 
49
+ console.log('\n━━━ 3. 稳定性护栏的严重性判定(哪类异常不该退进程) ━━━')
50
+ {
51
+ const { classifyFatalCandidate } = await import('../server/utils/stability-severity.ts')
52
+ const err = (code, message = 'boom') => Object.assign(new Error(message), { code })
53
+
54
+ // 外部设备不可达 → 不退进程。实测事故:一台 Modbus TCP 设备未启动
55
+ // (connect ECONNREFUSED 127.0.0.1:1502)使整个生产实例 exit 1。
56
+ check('ECONNREFUSED(设备未启动)→ upstream', classifyFatalCandidate(err('ECONNREFUSED')) === 'upstream')
57
+ check('ENOTFOUND(DNS 解析失败)→ upstream', classifyFatalCandidate(err('ENOTFOUND')) === 'upstream')
58
+ check('EHOSTUNREACH → upstream', classifyFatalCandidate(err('EHOSTUNREACH')) === 'upstream')
59
+ check('ENETUNREACH → upstream', classifyFatalCandidate(err('ENETUNREACH')) === 'upstream')
60
+ check('undici UND_ERR_CONNECT_TIMEOUT → upstream', classifyFatalCandidate(err('UND_ERR_CONNECT_TIMEOUT')) === 'upstream')
61
+ check('无 code 但 message 带 ECONNREFUSED → upstream',
62
+ classifyFatalCandidate(new Error('connect ECONNREFUSED 127.0.0.1:1502')) === 'upstream')
63
+
64
+ // 其余既有分级不得回退
65
+ check('ECONNRESET(客户端硬断)→ socket', classifyFatalCandidate(err('ECONNRESET')) === 'socket')
66
+ check('EPIPE → socket', classifyFatalCandidate(err('EPIPE')) === 'socket')
67
+ check('SQLITE_BUSY → db-busy', classifyFatalCandidate(err('SQLITE_BUSY')) === 'db-busy')
68
+ check('database is locked(无 code)→ db-busy', classifyFatalCandidate(new Error('database is locked')) === 'db-busy')
69
+ check('omp API 4xx → engine', classifyFatalCandidate(new Error('omp API 500: upstream failed')) === 'engine')
70
+
71
+ // fail-fast 语义必须保住:真实缺陷仍要退进程
72
+ check('密钥校验失败 → fatal(安全插件 fail-fast 依赖)', classifyFatalCandidate(new Error('生产环境必须设置 NUXT_SESSION_PASSWORD')) === 'fatal')
73
+ check('任意未分类异常 → fatal', classifyFatalCandidate(new Error('unexpected token in JSON at position 0')) === 'fatal')
74
+ check('null → fatal(不放过未知形态)', classifyFatalCandidate(null) === 'fatal')
75
+ check('非 Error 值(字符串)→ fatal', classifyFatalCandidate('something odd') === 'fatal')
76
+ // 引擎名只允许出现在首行:stack 里偶然含引擎名不得把真实错误降级
77
+ check('引擎名仅在 stack 中 → 不降级',
78
+ classifyFatalCandidate(Object.assign(new Error('TypeError: x is not a function'), { stack: 'TypeError: x is not a function\n at omp API (x.ts:1)' })) === 'fatal')
79
+ }
80
+
49
81
  console.log(`\n━━━ 结果:${pass} passed, ${fail} failed ━━━\n`)
50
82
  process.exit(fail === 0 ? 0 : 1)
@@ -378,16 +378,16 @@ let channelB
378
378
 
379
379
  // KB 回环:kb_index 入库独特自然语句 → kb_search 命中(嵌入/分词对自然语句才可靠);kb_store 沉淀经验
380
380
  const marker = `三系统验证密语${Date.now().toString(36)}:青花瓷泵站三十七号叶片于黄昏完成校准`
381
- const idx = await invoke(toolInvoker, 'kb_index', {
381
+ const idx = await invoke(toolInvoker, 'kb_index', { // rag 负载下事件循环停顿可达 60s+,放宽超时
382
382
  title: `三系统集成验证 ${Date.now().toString(36)}`,
383
383
  content: `# 三系统集成验证\n\n${marker}。\n\n本文由 three-system-e2e 经 rag-bridge 插件写入,验证 文档写盘 → 向量索引 → kb_search 检索 全链路。\n`,
384
384
  tags: ['e2e'],
385
- })
385
+ }, 120000)
386
386
  ok(resultText(idx).includes('已入库'), 'kb_index 文档入库成功', resultText(idx).slice(0, 80))
387
387
  let hit = ''
388
- for (let i = 0; i < 10; i++) { // 首次检索可能触发 BGE-M3 模型加载,放宽到 10×3s
388
+ for (let i = 0; i < 20; i++) { // 首次检索可能触发 BGE-M3 加载/异步索引落盘,放宽到 20×3s
389
389
  await sleep(3000)
390
- const s = await invoke(toolInvoker, 'kb_search', { query: marker })
390
+ const s = await invoke(toolInvoker, 'kb_search', { query: marker }, 120000)
391
391
  hit = resultText(s)
392
392
  if (hit.includes(marker)) break
393
393
  }
@@ -400,7 +400,7 @@ let channelB
400
400
  solution: '先查产线 LineRun 是否活动、节点 publishIntervalMs 是否在节拍上,再缩窗重试。',
401
401
  key_lessons: ['空窗先查打标窗口', 'bucket 5s 可对齐多节点'],
402
402
  tags: ['e2e', 'daq'],
403
- })
403
+ }, 120000)
404
404
  ok(resultText(store).includes('已沉淀'), 'kb_store 经验沉淀成功', resultText(store).slice(0, 80))
405
405
 
406
406
  // 跨通道:分析组长 → 控制组长(require_reply;用频道 ID 定向,避免跨账号同名频道歧义)
@@ -628,7 +628,7 @@ let runId = ''
628
628
  return (envelopeData(members) ?? []).find(m => m?.role === 'lead') ?? leadA
629
629
  }
630
630
  let gone = false
631
- for (let i = 0; i < 20 && !gone; i++) {
631
+ for (let i = 0; i < 40 && !gone; i++) { // 共享实例 agent 运行时重建慢,60s 窗
632
632
  await sleep(1500)
633
633
  const fl = await freshLead()
634
634
  const h = await raw('GET', `${BASE}/api/plugins/diag-bridge/health`, { token: userToken })