agentworkshop 0.7.40 → 0.7.41
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.
- package/.output/nitro.json +1 -1
- package/.output/public/_nuxt/{CzOCSGLe.js → BN16aRdQ.js} +1 -1
- package/.output/public/_nuxt/BWVhZntO.js +1 -0
- package/.output/public/_nuxt/{Dihnimlk.js → C0ZrS1KL.js} +1 -1
- package/.output/public/_nuxt/{Dr9v6C0f.js → C4s_niy-.js} +2 -2
- package/.output/public/_nuxt/D6mbFA-a.js +1 -0
- package/.output/public/_nuxt/{DcsCcxp4.js → D98NH8kG.js} +1 -1
- package/.output/public/_nuxt/{C_iYD-EB.js → DGbd-UiG.js} +1 -1
- package/.output/public/_nuxt/{DCn7QZ5N.js → Dap5PmTL.js} +1 -1
- package/.output/public/_nuxt/DvLxILQP.js +1 -0
- package/.output/public/_nuxt/{DZshyNLa.js → IREZ1eC2.js} +1 -1
- package/.output/public/_nuxt/_id_.D9ymtHB5.css +1 -0
- package/.output/public/_nuxt/_id_.jf-_8E_c.css +1 -0
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/78a295bb-2072-48a4-8b46-6d545fdae743.json +1 -0
- package/.output/public/_nuxt/ca4TJ3Op.js +1 -0
- package/.output/public/_nuxt/daq.DjSgY94l.css +1 -0
- package/.output/public/_nuxt/{BP-NyKVG.js → eqq3wWQF.js} +1 -1
- package/.output/server/chunks/_/nitro.mjs +886 -673
- package/.output/server/chunks/_/nitro.mjs.map +1 -1
- package/.output/server/chunks/build/{TownView-BuLAsmpR.mjs → TownView--otsnr_7.mjs} +4 -4
- package/.output/server/chunks/build/{TownView-BuLAsmpR.mjs.map → TownView--otsnr_7.mjs.map} +1 -1
- package/.output/server/chunks/build/{_id_-BU1wfOID.mjs → _id_-Brv-JSvr.mjs} +141 -140
- package/.output/server/chunks/build/_id_-Brv-JSvr.mjs.map +1 -0
- package/.output/server/chunks/build/{_id_-BYsEua57.mjs → _id_-D9pl1OZy.mjs} +37 -36
- package/.output/server/chunks/build/_id_-D9pl1OZy.mjs.map +1 -0
- package/.output/server/chunks/build/{_wsId_-BxlDcJHg.mjs → _wsId_-BASfDf0s.mjs} +4 -4
- package/.output/server/chunks/build/{_wsId_-BxlDcJHg.mjs.map → _wsId_-BASfDf0s.mjs.map} +1 -1
- package/.output/server/chunks/build/{daq-cgyjKmJu.mjs → daq-DkMvD8sg.mjs} +93 -92
- package/.output/server/chunks/build/daq-DkMvD8sg.mjs.map +1 -0
- package/.output/server/chunks/build/{dcw-CoTxBlAZ.mjs → dcw-C1jgVJkO.mjs} +3 -3
- package/.output/server/chunks/build/{dcw-CoTxBlAZ.mjs.map → dcw-C1jgVJkO.mjs.map} +1 -1
- package/.output/server/chunks/build/{logs-BSOeZwwI.mjs → logs--dByNyhw.mjs} +2 -2
- package/.output/server/chunks/build/{logs-BSOeZwwI.mjs.map → logs--dByNyhw.mjs.map} +1 -1
- package/.output/server/chunks/build/{pages-DFxjn9Xa.mjs → pages-B0bUWnC6.mjs} +3 -3
- package/.output/server/chunks/build/{pages-DFxjn9Xa.mjs.map → pages-B0bUWnC6.mjs.map} +1 -1
- package/.output/server/chunks/build/{town-qMPql9Se.mjs → town-CoQdblbZ.mjs} +4 -4
- package/.output/server/chunks/build/{town-qMPql9Se.mjs.map → town-CoQdblbZ.mjs.map} +1 -1
- package/.output/server/chunks/build/{useDaqStream-CIBaiGAl.mjs → useDaqStream-DfCFpb3p.mjs} +9 -3
- package/.output/server/chunks/build/useDaqStream-DfCFpb3p.mjs.map +1 -0
- package/.output/server/chunks/build/{useDcwStream-CBbiYnWe.mjs → useDcwStream-UPiX-3iO.mjs} +11 -2
- package/.output/server/chunks/build/useDcwStream-UPiX-3iO.mjs.map +1 -0
- package/.output/server/chunks/routes/api/workshop/channel-templates/_id/instantiate.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/channels.post.mjs +3 -3
- package/.output/server/chunks/routes/api/workshop/dcw/index5.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/lines/_id/start.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/lines/_id/stop.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/params/_id_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/params/_id_.patch.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/templates/_key_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/templates/_key_.patch.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/harnesses/_id/providers.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/harnesses.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/index.put.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/index5.get.mjs +3 -1
- package/.output/server/chunks/routes/api/workshop/index5.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/index6.get.mjs +3 -1
- package/.output/server/chunks/routes/api/workshop/index6.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/plugins/_name/disable.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/plugins/_name/enable.post.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.delete.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.put.mjs +1 -1
- package/.output/server/chunks/routes/api/workshop/workspaces/_id/channel-templates/_tplId_.post.mjs +1 -1
- package/.output/server/chunks/routes/renderer.mjs +2 -2
- package/.output/server/chunks/virtual/entry.mjs +9 -9
- package/.output/server/chunks/virtual/precomputed.mjs +1 -1
- package/.output/server/package.json +1 -1
- package/README-zh.md +5 -5
- package/README.md +6 -6
- package/app/composables/workshop/useDaqStream.ts +7 -4
- package/app/composables/workshop/useDcwStream.ts +8 -2
- package/app/pages/daq/[id].vue +9 -5
- package/app/pages/daq/index.vue +13 -10
- package/app/pages/dcw/[id].vue +8 -4
- package/docs/cli.en.md +2 -2
- package/docs/cli.md +2 -2
- package/docs/plugins.en.md +33 -1
- package/docs/plugins.md +29 -1
- package/docs/sdk.en.md +1 -1
- package/docs/sdk.md +1 -1
- package/package.json +3 -2
- package/scripts/_loop-biax-campaign.mjs +124 -0
- package/scripts/test-serial-bridge.mjs +418 -0
- package/scripts/ui/e2e-interactive.mjs +328 -0
- package/scripts/ui/seed-industrial-demo.mjs +171 -0
- package/server/api/workshop/daq/index.get.ts +3 -1
- package/server/api/workshop/dcw/index.get.ts +3 -0
- package/server/plugins/aw-plugins.ts +9 -7
- package/server/plugins-builtin/serial-bridge/client.mjs +210 -0
- package/server/plugins-builtin/serial-bridge/drivers.mjs +409 -0
- package/server/plugins-builtin/serial-bridge/i18n.json +62 -0
- package/server/plugins-builtin/serial-bridge/index.mjs +247 -0
- package/server/plugins-builtin/serial-bridge/lib.mjs +208 -0
- package/server/plugins-builtin/serial-bridge/rtu.mjs +168 -0
- package/server/services/workshop/daq/daq-controller.ts +7 -2
- package/server/services/workshop/daq/drivers.ts +78 -3
- package/server/services/workshop/dcw/dcw-controller.ts +5 -1
- package/server/services/workshop/dcw/drivers.ts +85 -2
- package/server/services/workshop/dcw/plugin-bridge.ts +45 -0
- package/server/services/workshop/plugins/host.mjs +30 -0
- package/shared/daq-protocol.ts +15 -0
- package/.output/public/_nuxt/BOt2SP4z.js +0 -1
- package/.output/public/_nuxt/ClILjvWH.js +0 -1
- package/.output/public/_nuxt/D8qvkfyY.js +0 -1
- package/.output/public/_nuxt/Du9ZJtnj.js +0 -1
- package/.output/public/_nuxt/_id_.CM4bIgKY.css +0 -1
- package/.output/public/_nuxt/_id_.bNV69Dtf.css +0 -1
- package/.output/public/_nuxt/builds/meta/f4707884-7cf4-4a31-80d2-e3d72b73745f.json +0 -1
- package/.output/public/_nuxt/daq.B0D3dPAc.css +0 -1
- package/.output/server/chunks/build/_id_-BU1wfOID.mjs.map +0 -1
- package/.output/server/chunks/build/_id_-BYsEua57.mjs.map +0 -1
- package/.output/server/chunks/build/daq-cgyjKmJu.mjs.map +0 -1
- package/.output/server/chunks/build/useDaqStream-CIBaiGAl.mjs.map +0 -1
- package/.output/server/chunks/build/useDcwStream-CBbiYnWe.mjs.map +0 -1
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 端到端浏览器交互测试 —— 真实 UI 点击/输入,覆盖登录→仪表盘→产线运营(停/开跑)→
|
|
3
|
+
* 产线详情写控(下发/读取)→数采(实时+详情)→数字孪生(3D/面板/画质)→Agent 工作台
|
|
4
|
+
* (进入控制台+发消息)→设置(主题色/暗色开关)→全页面渲染冒烟;全程收集控制台错误。
|
|
5
|
+
* 运行: AW_BASE=http://127.0.0.1:3021 node scripts/ui/e2e-interactive.mjs
|
|
6
|
+
*/
|
|
7
|
+
import { launch, openPage } from './lib.mjs'
|
|
8
|
+
import { mkdirSync } from 'node:fs'
|
|
9
|
+
|
|
10
|
+
const BASE = process.env.AW_BASE ?? 'http://127.0.0.1:3021'
|
|
11
|
+
const SHOTS = '.tmp-shots'
|
|
12
|
+
mkdirSync(SHOTS, { recursive: true })
|
|
13
|
+
|
|
14
|
+
let failed = 0, passed = 0
|
|
15
|
+
const check = (name, ok, detail = '') => {
|
|
16
|
+
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
17
|
+
if (ok) passed++; else failed++
|
|
18
|
+
}
|
|
19
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
20
|
+
|
|
21
|
+
const browser = await launch({ width: 1600, height: 900 })
|
|
22
|
+
const errors = []
|
|
23
|
+
const watch = (page, path) => {
|
|
24
|
+
page.on('pageerror', e => errors.push({ path, message: String(e?.message ?? e) }))
|
|
25
|
+
page.on('console', (m) => { if (m.type() === 'error') errors.push({ path, message: m.text().slice(0, 200) }) })
|
|
26
|
+
}
|
|
27
|
+
const clickText = (page, text, { exact = false } = {}) => page.evaluate(({ text, exact }) => {
|
|
28
|
+
const els = [...document.querySelectorAll('button, a, [role="button"], .ant-btn')]
|
|
29
|
+
const hit = els.find(e => exact ? (e.textContent || '').trim() === text : (e.textContent || '').includes(text))
|
|
30
|
+
if (hit) { hit.click(); return true }
|
|
31
|
+
return false
|
|
32
|
+
}, { text, exact })
|
|
33
|
+
const setInputNear = (page, labelText, value) => page.evaluate(({ labelText, value }) => {
|
|
34
|
+
const inputs = [...document.querySelectorAll('input:not([type=hidden]):not([disabled])')]
|
|
35
|
+
for (const inp of inputs) {
|
|
36
|
+
const ctx = inp.closest('label, .ant-form-item, div, p, span')?.textContent ?? ''
|
|
37
|
+
if (ctx.includes(labelText)) {
|
|
38
|
+
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
|
|
39
|
+
setter.call(inp, value)
|
|
40
|
+
inp.dispatchEvent(new Event('input', { bubbles: true }))
|
|
41
|
+
inp.dispatchEvent(new Event('change', { bubbles: true }))
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false
|
|
46
|
+
}, { labelText, value })
|
|
47
|
+
const bodyHas = (page, text) => page.evaluate(t => document.body.innerText.includes(t), text)
|
|
48
|
+
|
|
49
|
+
// ════ ① 认证体验:访客态渲染 + cookie 登录后身份显示 ════
|
|
50
|
+
{
|
|
51
|
+
console.log('\n── ① 认证体验(访客态 → 管理员) ──')
|
|
52
|
+
const page = await browser.newPage()
|
|
53
|
+
watch(page, '/guest')
|
|
54
|
+
await page.goto(BASE + '/', { waitUntil: 'networkidle2', timeout: 60000 }).catch(() => {})
|
|
55
|
+
await sleep(2500)
|
|
56
|
+
const guestShell = await page.evaluate(() => !!document.querySelector('.app-header, .user-chip') && document.body.innerText.trim().length > 200)
|
|
57
|
+
const guestBadge = await page.evaluate(() => /访客|guest|anonymous/i.test(document.body.innerText))
|
|
58
|
+
check('访客态渲染应用壳(游客可浏览,身份=访客)', guestShell && guestBadge, `badge=${guestBadge}`)
|
|
59
|
+
// cookie 登录(token 写入)→ 身份变为管理员
|
|
60
|
+
const login = await (await fetch(BASE + '/api/users/login', {
|
|
61
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({ email: 'admin@awshop.local', password: 'admin123' }),
|
|
63
|
+
})).json()
|
|
64
|
+
await page.setCookie({ name: 'token', value: login.data.token, path: '/' })
|
|
65
|
+
await page.reload({ waitUntil: 'networkidle2' })
|
|
66
|
+
await sleep(2500)
|
|
67
|
+
const adminShown = await page.evaluate(() => /admin/i.test(document.querySelector('.user-chip')?.textContent ?? ''))
|
|
68
|
+
check('cookie 登录后身份显示 admin', adminShown, await page.evaluate(() => (document.querySelector('.user-chip')?.textContent ?? '').trim().slice(0, 24)))
|
|
69
|
+
await page.screenshot({ path: `${SHOTS}/e2e-01-auth.png` })
|
|
70
|
+
await page.close()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ════ 已登录会话 ════
|
|
74
|
+
const login = await (await fetch(BASE + '/api/users/login', {
|
|
75
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
76
|
+
body: JSON.stringify({ email: 'admin@awshop.local', password: 'admin123' }),
|
|
77
|
+
})).json()
|
|
78
|
+
const token = login.data.token
|
|
79
|
+
const apiGet = async u => (await fetch(BASE + u, { headers: { authorization: `Bearer ${token}` } })).json()
|
|
80
|
+
const page = await openPage(browser, { token, dark: true, width: 1600, height: 900, deviceScaleFactor: 1 })
|
|
81
|
+
watch(page, 'global')
|
|
82
|
+
|
|
83
|
+
// ════ ② 仪表盘 ════
|
|
84
|
+
{
|
|
85
|
+
console.log('\n── ② 仪表盘 ──')
|
|
86
|
+
await page.goto(BASE + '/', { waitUntil: 'networkidle2', timeout: 60000 })
|
|
87
|
+
await sleep(3000)
|
|
88
|
+
const txt = await page.evaluate(() => document.body.innerText)
|
|
89
|
+
check('仪表盘渲染非空', txt.trim().length > 200, `${txt.length} chars`)
|
|
90
|
+
await page.screenshot({ path: `${SHOTS}/e2e-02-dashboard.png` })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ════ ③ 产线运营:停止 / 开跑(经 API 校验真实状态) ════
|
|
94
|
+
{
|
|
95
|
+
console.log('\n── ③ 产线运营(停止/开跑) ──')
|
|
96
|
+
await page.goto(BASE + '/dcw', { waitUntil: 'networkidle2', timeout: 60000 })
|
|
97
|
+
await sleep(2500)
|
|
98
|
+
check('演示产线卡片渲染', await bodyHas(page, '演示线'))
|
|
99
|
+
const lines = (await apiGet('/api/workshop/dcw/lines')).data?.lines ?? []
|
|
100
|
+
const target = lines.find(l => l.name.startsWith('演示线2')) ?? lines.find(l => l.name.startsWith('演示线'))
|
|
101
|
+
if (!target) { check('定位演示产线', false) }
|
|
102
|
+
else {
|
|
103
|
+
// 卡片定位:含线名文本 + 内有按钮的**最小** div(截断阈值防页面根逃逸);句柄直操作
|
|
104
|
+
const findCard = async (lineName) => {
|
|
105
|
+
const h = await page.evaluateHandle((ln) => {
|
|
106
|
+
const divs = [...document.querySelectorAll('div')]
|
|
107
|
+
.filter(d => (d.textContent || '').includes(ln) && /配方/.test(d.textContent || '') && d.textContent.length < 1500 && d.querySelectorAll('button').length > 0)
|
|
108
|
+
return divs.sort((a, b) => a.textContent.length - b.textContent.length)[0] ?? null
|
|
109
|
+
}, lineName)
|
|
110
|
+
const el = h.asElement()
|
|
111
|
+
if (!el) { await h.dispose(); return null }
|
|
112
|
+
return el
|
|
113
|
+
}
|
|
114
|
+
const card1 = await findCard(target.name)
|
|
115
|
+
if (!card1) { check('定位演示线2 卡片', false) }
|
|
116
|
+
const stopped = await page.evaluate((card) => {
|
|
117
|
+
const btn = [...card.querySelectorAll('button')].find(b => /停止/.test(b.textContent))
|
|
118
|
+
if (!btn) return 'already-stopped'
|
|
119
|
+
btn.click(); return 'clicked'
|
|
120
|
+
}, card1)
|
|
121
|
+
await card1.dispose()
|
|
122
|
+
await sleep(2500)
|
|
123
|
+
const states1 = (await apiGet('/api/workshop/dcw')).data?.lineStates ?? []
|
|
124
|
+
const s1 = states1.find(s => s.lineId === target.id)?.active === true
|
|
125
|
+
check('UI 停产线生效(卡片 ■停止 → 批次关闭)', stopped === 'clicked' && !s1, String(stopped))
|
|
126
|
+
const card2 = await findCard(target.name)
|
|
127
|
+
if (!card2) { check('定位卡片(开跑)', false) }
|
|
128
|
+
// 两拍:先选配方(若无),等 Vue 生效,再点开跑
|
|
129
|
+
await page.evaluate((card) => {
|
|
130
|
+
const sel = card.querySelector('select')
|
|
131
|
+
if (sel && !sel.value && sel.options.length > 1) {
|
|
132
|
+
const setter = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'value').set
|
|
133
|
+
setter.call(sel, sel.options[1].value)
|
|
134
|
+
sel.dispatchEvent(new Event('change', { bubbles: true }))
|
|
135
|
+
}
|
|
136
|
+
}, card2)
|
|
137
|
+
await sleep(600)
|
|
138
|
+
const started = await page.evaluate((card) => {
|
|
139
|
+
const btn = [...card.querySelectorAll('button')].find(b => /开跑|开始/.test(b.textContent))
|
|
140
|
+
if (!btn) return 'no-start-btn'
|
|
141
|
+
btn.click(); return 'clicked'
|
|
142
|
+
}, card2)
|
|
143
|
+
await card2.dispose()
|
|
144
|
+
await sleep(3500)
|
|
145
|
+
const states2 = (await apiGet('/api/workshop/dcw')).data?.lineStates ?? []
|
|
146
|
+
const s2 = states2.find(s => s.lineId === target.id)?.active === true
|
|
147
|
+
check('UI 开跑生效(批次激活)', started === 'clicked' && s2, String(started))
|
|
148
|
+
await page.screenshot({ path: `${SHOTS}/e2e-03-dcw-lines.png` })
|
|
149
|
+
await page.screenshot({ path: `${SHOTS}/e2e-03-dcw-lines.png` })
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ════ ④ 产线详情:设定值下发 + 读取(真实 PLC 链路) ════
|
|
154
|
+
{
|
|
155
|
+
console.log('\n── ④ 产线详情写控(下发/读取) ──')
|
|
156
|
+
const lines = (await apiGet('/api/workshop/dcw/lines')).data?.lines ?? []
|
|
157
|
+
const demoLine = lines.find(l => l.name.startsWith('演示线1'))
|
|
158
|
+
const dcwAll = (await apiGet('/api/workshop/dcw')).data?.nodes ?? []
|
|
159
|
+
const node = dcwAll.find(n => n.name.includes('Coating Oven'))
|
|
160
|
+
if (!demoLine || !node) { check('定位演示产线/节点', false) }
|
|
161
|
+
else {
|
|
162
|
+
await page.goto(`${BASE}/dcw/${demoLine.id}`, { waitUntil: 'networkidle2', timeout: 60000 })
|
|
163
|
+
await sleep(3500)
|
|
164
|
+
const before = node.value
|
|
165
|
+
const target = Math.min(node.max - 1, Math.max(node.min + 1, Math.round(before ?? 130) + 5))
|
|
166
|
+
const filled = await page.evaluate(({ ph, value }) => {
|
|
167
|
+
const inp = [...document.querySelectorAll('input[type=number]')].find(i => (i.placeholder || '').replace(/\s/g, '') === ph.replace(/\s/g, ''))
|
|
168
|
+
if (!inp) return false
|
|
169
|
+
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
|
|
170
|
+
setter.call(inp, value)
|
|
171
|
+
inp.dispatchEvent(new Event('input', { bubbles: true }))
|
|
172
|
+
inp.dispatchEvent(new Event('change', { bubbles: true }))
|
|
173
|
+
return true
|
|
174
|
+
}, { ph: `${node.min}~${node.max}`, value: String(target) })
|
|
175
|
+
const clicked = await page.evaluate(() => {
|
|
176
|
+
const btn = [...document.querySelectorAll('button')].find(b => b.textContent.trim() === '下发')
|
|
177
|
+
if (!btn) return false
|
|
178
|
+
btn.click(); return true
|
|
179
|
+
})
|
|
180
|
+
await sleep(5000)
|
|
181
|
+
const after = (await apiGet('/api/workshop/dcw')).data?.nodes?.find(n => n.id === node.id)
|
|
182
|
+
const tol = Math.max(0.8, (node.max - node.min) * 0.01)
|
|
183
|
+
check('UI 下发设定值 → PLC 生效(回读一致)', filled && clicked && after?.value != null && Math.abs((after.value ?? 0) - target) <= tol, `before=${before} target=${target} after=${after?.value} filled=${filled} clicked=${clicked}`)
|
|
184
|
+
const rc = await page.evaluate(() => {
|
|
185
|
+
const btn = [...document.querySelectorAll('button')].find(b => b.textContent.trim() === '读取')
|
|
186
|
+
if (!btn) return false
|
|
187
|
+
btn.click(); return true
|
|
188
|
+
})
|
|
189
|
+
await sleep(3000)
|
|
190
|
+
const after2 = (await apiGet('/api/workshop/dcw')).data?.nodes?.find(n => n.id === node.id)
|
|
191
|
+
check('UI 读取按钮 → PLC 读数回填', rc === true && after2?.readValue != null, `readValue=${after2?.readValue}`)
|
|
192
|
+
await page.screenshot({ path: `${SHOTS}/e2e-04-line-detail.png` })
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ════ ⑤ 数采中心:实时性 + 节点详情 ════
|
|
197
|
+
{
|
|
198
|
+
console.log('\n── ⑤ 数采中心 ──')
|
|
199
|
+
await page.goto(BASE + '/daq', { waitUntil: 'networkidle2', timeout: 60000 })
|
|
200
|
+
await sleep(3000)
|
|
201
|
+
const snap1 = await page.evaluate(() => document.body.innerText.slice(0, 4000))
|
|
202
|
+
await sleep(7000)
|
|
203
|
+
const snap2 = await page.evaluate(() => document.body.innerText.slice(0, 4000))
|
|
204
|
+
check('数采实时渲染(7s 内 DOM 数值变化)', snap1 !== snap2)
|
|
205
|
+
const daqList = await apiGet('/api/workshop/daq')
|
|
206
|
+
const dn = (daqList.data?.nodes ?? []).find(n => n.name.includes('Coating Oven'))
|
|
207
|
+
if (dn) {
|
|
208
|
+
await page.goto(`${BASE}/daq/${dn.id}`, { waitUntil: 'networkidle2', timeout: 60000 })
|
|
209
|
+
await sleep(3500)
|
|
210
|
+
const chart = await page.evaluate(() => !!document.querySelector('canvas, svg'))
|
|
211
|
+
check('数采节点详情(图表渲染)', chart)
|
|
212
|
+
await page.screenshot({ path: `${SHOTS}/e2e-05-daq-detail.png` })
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ════ ⑥ 数字孪生:3D + 实时面板 + 交互 ════
|
|
217
|
+
{
|
|
218
|
+
console.log('\n── ⑥ 数字孪生 ──')
|
|
219
|
+
await page.goto(BASE + '/town', { waitUntil: 'networkidle2', timeout: 60000 })
|
|
220
|
+
await sleep(9000)
|
|
221
|
+
const scene = await page.evaluate(() => {
|
|
222
|
+
const canvas = [...document.querySelectorAll('canvas')].find(c => c.width > 300)
|
|
223
|
+
const fps = (document.body.innerText.match(/(\d{2,3})\s*FPS/i) ?? [])[1]
|
|
224
|
+
return { canvas: !!canvas, fps: fps ? Number(fps) : 0 }
|
|
225
|
+
})
|
|
226
|
+
check('3D 画布渲染 + FPS', scene.canvas && scene.fps > 5, `fps=${scene.fps}`)
|
|
227
|
+
const t1 = await page.evaluate(() => document.body.innerText.slice(0, 5000))
|
|
228
|
+
await sleep(6000)
|
|
229
|
+
const t2 = await page.evaluate(() => document.body.innerText.slice(0, 5000))
|
|
230
|
+
check('孪生面板实时刷新', t1 !== t2)
|
|
231
|
+
check('画质切换交互(高清)', await clickText(page, '高清'))
|
|
232
|
+
await sleep(1500)
|
|
233
|
+
check('重置视角交互', await clickText(page, '重置视角'))
|
|
234
|
+
await sleep(2000)
|
|
235
|
+
await page.screenshot({ path: `${SHOTS}/e2e-06-town.png` })
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ════ ⑦ Agent 工作台:进入控制台 + 发消息 ════
|
|
239
|
+
{
|
|
240
|
+
console.log('\n── ⑦ Agent 工作台 ──')
|
|
241
|
+
await page.goto(BASE + '/workshop', { waitUntil: 'networkidle2', timeout: 60000 })
|
|
242
|
+
await sleep(2500)
|
|
243
|
+
const cardOk = await clickText(page, '进入控制台')
|
|
244
|
+
await sleep(3500)
|
|
245
|
+
check('进入工作区控制台', cardOk, page.url())
|
|
246
|
+
// 任务下发 composer(首行=标题):真实下发 → API 断言任务创建
|
|
247
|
+
const wsId = page.url().match(/workshop\/w\/([a-z0-9-]+)/i)?.[1] ?? null
|
|
248
|
+
const sent = await page.evaluate(() => {
|
|
249
|
+
const any = [...document.querySelectorAll('textarea')][0]
|
|
250
|
+
if (!any) return 'no-input'
|
|
251
|
+
const st = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set
|
|
252
|
+
st.call(any, 'e2e 交互测试:回报当前产线状态')
|
|
253
|
+
any.dispatchEvent(new Event('input', { bubbles: true }))
|
|
254
|
+
any.focus()
|
|
255
|
+
return 'typed'
|
|
256
|
+
})
|
|
257
|
+
if (sent === 'typed') {
|
|
258
|
+
let postStatus = 0, dupCode = false, postId = ''
|
|
259
|
+
page.on('response', async (r) => {
|
|
260
|
+
if (!(r.request().method() === 'POST' && r.url().includes('/tasks'))) return
|
|
261
|
+
postStatus = r.status()
|
|
262
|
+
try { const j = await r.json(); if (j.code === 'TASK_DUPLICATE') { dupCode = true; const m = String(j.message ?? '').split('(').pop()?.split(',')[0] ?? ''; postId = m } }
|
|
263
|
+
catch {}
|
|
264
|
+
})
|
|
265
|
+
await page.evaluate(() => {
|
|
266
|
+
const btn = [...document.querySelectorAll('button')].find(b => /发送/.test(b.title ?? ''))
|
|
267
|
+
btn ? btn.click() : page.keyboard.press('Enter')
|
|
268
|
+
})
|
|
269
|
+
await sleep(4500)
|
|
270
|
+
const dispatchOk = postStatus === 200 || dupCode
|
|
271
|
+
check('UI 任务下发成功(200 创建或 409 查重守卫)', dispatchOk, `status=${postStatus} dup=${dupCode}`)
|
|
272
|
+
// 清理在途任务(goal 模式 mock lead 不会自动收口)
|
|
273
|
+
if (postId) await fetch(BASE + '/api/workshop/channels/0df82b5d-9f66-4495-9c52-1163f09373bb/tasks/' + postId + '/cancel', { method: 'POST', headers: { 'authorization': `Bearer ${token}`, 'content-type': 'application/json' }, body: '{}' }).catch(() => {})
|
|
274
|
+
}
|
|
275
|
+
else check('任务 composer 存在', false, String(sent))
|
|
276
|
+
await page.screenshot({ path: `${SHOTS}/e2e-07-workshop.png` })
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ════ ⑧ 系统设置:主题色 + 暗色开关 ════
|
|
280
|
+
{
|
|
281
|
+
console.log('\n── ⑧ 系统设置 ──')
|
|
282
|
+
await page.goto(BASE + '/settings', { waitUntil: 'networkidle2', timeout: 60000 })
|
|
283
|
+
await sleep(2000)
|
|
284
|
+
const accentBefore = await page.evaluate(() => localStorage.getItem('app'))
|
|
285
|
+
const picked = await clickText(page, '琥珀')
|
|
286
|
+
await sleep(1200)
|
|
287
|
+
const accentAfter = await page.evaluate(() => localStorage.getItem('app'))
|
|
288
|
+
check('主题色切换交互(品牌→琥珀)', picked && accentBefore !== accentAfter)
|
|
289
|
+
await clickText(page, '恢复默认')
|
|
290
|
+
await sleep(800)
|
|
291
|
+
const sw = await page.evaluate(() => {
|
|
292
|
+
const el = document.querySelector('.ant-switch, button[role=switch]')
|
|
293
|
+
if (el) { el.click(); return true }
|
|
294
|
+
return false
|
|
295
|
+
})
|
|
296
|
+
await sleep(1000)
|
|
297
|
+
check('暗色开关交互', sw)
|
|
298
|
+
await page.screenshot({ path: `${SHOTS}/e2e-08-settings.png` })
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ════ ⑨ 其余页面渲染冒烟 ════
|
|
302
|
+
{
|
|
303
|
+
console.log('\n── ⑨ 其余页面渲染冒烟 ──')
|
|
304
|
+
for (const p of ['/logs', '/monitor', '/tokens', '/users', '/permissions', '/plugins', '/aml']) {
|
|
305
|
+
await page.goto(BASE + p, { waitUntil: 'networkidle2', timeout: 45000 }).catch(() => {})
|
|
306
|
+
await sleep(1500)
|
|
307
|
+
const len = await page.evaluate(() => document.body.innerText.trim().length)
|
|
308
|
+
check(`渲染 ${p}`, len > 80, `${len} chars`)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ════ ⑩ 控制台错误汇总(登录 401 属预期;Vue Hydration 警告单列) ════
|
|
313
|
+
{
|
|
314
|
+
console.log('\n── ⑩ 控制台错误汇总 ──')
|
|
315
|
+
const benign = /favicon|Download the Vue Devtools|sourcemap| autob |WebSocket.*closed|AbstractChart|ResizeObserver|Failed to load resource.*401|Failed to load resource.*409/i
|
|
316
|
+
const real = errors.filter(e => !benign.test(e.message))
|
|
317
|
+
const hydration = real.filter(e => /Hydration/i.test(e.message))
|
|
318
|
+
const fatal = real.filter(e => !/Hydration/i.test(e.message))
|
|
319
|
+
check('无致命前端错误', fatal.length === 0, `${fatal.length}/${errors.length} 条${fatal.length ? ': ' + fatal.slice(0, 2).map(e => '[' + e.path + '] ' + e.message.slice(0, 120)).join(' | ') : ''}(另有 Hydration 警告 ${hydration.length} 条)`)
|
|
320
|
+
if (hydration.length) {
|
|
321
|
+
console.log(` ⚠ 已知非致命警告:Vue SSR Hydration mismatch ×${hydration.length}(页面功能与渲染不受影响;记录为待修前端瑕疵)`)
|
|
322
|
+
passed++
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
await browser.close()
|
|
327
|
+
console.log(`\n${failed === 0 ? '✅ 端到端交互测试全部通过' : `❌ ${failed} 项失败`}(共 ${passed + failed} 项断言)`)
|
|
328
|
+
process.exit(failed === 0 ? 0 : 1)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工业演示现场布线 —— 对真实 PLC 模拟器(:4010)做一次「可登录即可见」的完整接装:
|
|
3
|
+
* ①每台模拟器设备 → 平台节点(DCW 写控 / DAQ 数采,driverConfig 取自模拟器对接导出)
|
|
4
|
+
* ②每台设备一个数字孪生模型(modelRef=设备模型库)并把节点双向绑定到孪生
|
|
5
|
+
* ③产线+产品+配方(全量程窗)并开跑 → 数采在活动批次内真实采样
|
|
6
|
+
* ④逐节点 test-driver 真连通验证 + 一次受治理参数写→回读 + 数采样本增长
|
|
7
|
+
* 幂等:按「演示」标记清理上一次的演示线,再重建。
|
|
8
|
+
* 运行: AW_BASE=http://127.0.0.1:3001 node scripts/ui/seed-industrial-demo.mjs
|
|
9
|
+
*/
|
|
10
|
+
const BASE = process.env.AW_BASE ?? 'http://127.0.0.1:3001'
|
|
11
|
+
const SIM = process.env.E2E_SIM_API ?? 'http://127.0.0.1:4010'
|
|
12
|
+
const ADMIN = { email: process.env.E2E_USER ?? 'admin@awshop.local', password: process.env.E2E_PASS ?? 'admin123' }
|
|
13
|
+
|
|
14
|
+
let failed = 0
|
|
15
|
+
const check = (name, ok, detail = '') => {
|
|
16
|
+
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
17
|
+
if (!ok) failed++
|
|
18
|
+
}
|
|
19
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
20
|
+
|
|
21
|
+
const login = await fetch(`${BASE}/api/users/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(ADMIN) }).then(r => r.json())
|
|
22
|
+
const token = login.data?.token
|
|
23
|
+
check('管理员登录', !!token, `${ADMIN.email} ${login.message ?? ''}`)
|
|
24
|
+
if (!token) process.exit(1)
|
|
25
|
+
const H = { 'content-type': 'application/json', 'authorization': `Bearer ${token}` }
|
|
26
|
+
const api = async (m, u, b) => (await fetch(BASE + u, { method: m, headers: H, body: b ? JSON.stringify(b) : undefined })).json()
|
|
27
|
+
// 列表解包:data 可能是数组或 {lines/nodes/twins: [...]}(各路由形状不一)
|
|
28
|
+
const arr = (d, key) => (Array.isArray(d) ? d : d?.[key] ?? d?.nodes ?? d?.lines ?? d?.twins ?? [])
|
|
29
|
+
const sim = async (m, u, b) => (await fetch(SIM + u, { method: m, headers: { 'content-type': 'application/json' }, body: b ? JSON.stringify(b) : undefined })).json()
|
|
30
|
+
|
|
31
|
+
// ── ① 模拟器设备清单(真实五协议) ──
|
|
32
|
+
const devices = (await sim('GET', '/api/nodes')).data ?? []
|
|
33
|
+
check('PLC 模拟器在线', devices.length > 0, `${devices.length} 台设备: ${devices.map(d => d.name).join(' / ')}`)
|
|
34
|
+
|
|
35
|
+
// ── ② 清理上一轮演示实体(按「演示」标记) ──
|
|
36
|
+
const oldLines = arr((await api('GET', '/api/workshop/dcw/lines')).data, 'lines')
|
|
37
|
+
for (const l of oldLines.filter(x => x.name.startsWith('演示线'))) {
|
|
38
|
+
await api('DELETE', `/api/workshop/dcw/lines/${l.id}?purge=1`)
|
|
39
|
+
}
|
|
40
|
+
const oldTwins = arr((await api('GET', '/api/workshop/device-twins')).data, 'twins')
|
|
41
|
+
for (const n of arr((await api('GET', '/api/workshop/daq')).data, 'nodes').filter(x => x.name.startsWith('演示·'))) {
|
|
42
|
+
await api('DELETE', `/api/workshop/daq/${n.id}`)
|
|
43
|
+
}
|
|
44
|
+
for (const n of arr((await api('GET', '/api/workshop/dcw')).data, 'nodes').filter(x => x.name.startsWith('演示·'))) {
|
|
45
|
+
await api('DELETE', `/api/workshop/dcw/${n.id}`)
|
|
46
|
+
}
|
|
47
|
+
for (const t of oldTwins.filter(x => x.name.startsWith('演示·'))) {
|
|
48
|
+
await api('DELETE', `/api/workshop/device-twins/${t.id}`)
|
|
49
|
+
}
|
|
50
|
+
console.log(' · 旧演示实体已清理')
|
|
51
|
+
|
|
52
|
+
// ── ③ 逐设备建节点(SP→DCW 写控;标量 PV→DAQ 数采;driverConfig 取自模拟器导出) ──
|
|
53
|
+
// 模型映射:按设备语义挑数字孪生模型库里的模型
|
|
54
|
+
const MODEL_OF = {
|
|
55
|
+
'Coating Oven PLC': 'caster', 'Remote RTU Temp Slave': 'power-cabinet',
|
|
56
|
+
'OPC UA Temp Controller': 'extruder', 'MQTT Temp Sensor': 'device-scanner',
|
|
57
|
+
'HTTP Flow Meter': 'pump',
|
|
58
|
+
}
|
|
59
|
+
const isSp = s => /SP/i.test(s.signal ?? '') || /^Set/i.test(s.signal ?? '')
|
|
60
|
+
const results = []
|
|
61
|
+
let lineIx = 0
|
|
62
|
+
const twins = []
|
|
63
|
+
for (const dev of devices) {
|
|
64
|
+
const exp = (await sim('GET', `/api/nodes/${dev.id}/export`)).data
|
|
65
|
+
const items = exp?.items ?? []
|
|
66
|
+
const spItem = items.find(i => isSp(i)) ?? null
|
|
67
|
+
const pvItems = items.filter(i => i !== spItem && (i.format ?? 'scalar') === 'scalar')
|
|
68
|
+
if (!spItem && pvItems.length === 0) continue
|
|
69
|
+
const modelKey = MODEL_OF[dev.name] ?? 'device-console'
|
|
70
|
+
const twin = (await api('POST', '/api/workshop/device-twins', {
|
|
71
|
+
name: `演示·${dev.name}`,
|
|
72
|
+
modelRef: modelKey,
|
|
73
|
+
posX: -14 + lineIx * 7,
|
|
74
|
+
posZ: 6 + (lineIx % 2) * 5,
|
|
75
|
+
rotationY: 0,
|
|
76
|
+
})).data?.twin
|
|
77
|
+
if (!twin) {
|
|
78
|
+
check(`孪生创建 ${dev.name}`, false)
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
twins.push(twin)
|
|
82
|
+
|
|
83
|
+
let hostLine = null
|
|
84
|
+
const bindNode = async (nodeId) => {
|
|
85
|
+
const r = await api('POST', `/api/workshop/dcw/${nodeId}/bind`, { deviceId: twin.id }).catch(() => null)
|
|
86
|
+
return r?.code === 0
|
|
87
|
+
}
|
|
88
|
+
const bindDaq = async (nodeId) => {
|
|
89
|
+
const r = await api('POST', `/api/workshop/daq/${nodeId}/bind`, { deviceId: twin.id }).catch(() => null)
|
|
90
|
+
return r?.code === 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// SP → 写控节点 + 自成产线(开跑后本线批次活动,数采采样门放行)
|
|
94
|
+
if (spItem) {
|
|
95
|
+
lineIx++
|
|
96
|
+
const line = (await api('POST', '/api/workshop/dcw/lines', { name: `演示线${lineIx}·${dev.name}` })).data?.line
|
|
97
|
+
const product = (await api('POST', '/api/workshop/dcw/products', { lineId: line.id, name: `演示产品${lineIx}` })).data?.product
|
|
98
|
+
hostLine = line
|
|
99
|
+
const cfg = spItem.driverConfig
|
|
100
|
+
const dcw = (await api('POST', '/api/workshop/dcw', {
|
|
101
|
+
templateRef: 'dcw-temp-sp', name: `演示·${dev.name}·${spItem.signal}`,
|
|
102
|
+
driver: spItem.driver, driverConfig: cfg,
|
|
103
|
+
unit: spItem.unit ?? '℃', decimals: spItem.decimals ?? 1,
|
|
104
|
+
min: spItem.min ?? 0, max: spItem.max ?? 300,
|
|
105
|
+
readIntervalMs: 3000, lineId: line.id, semantics: `模拟器设备「${dev.name}」的 ${spItem.signal}(真实 ${spItem.driver} 写控链路)`,
|
|
106
|
+
})).data?.node
|
|
107
|
+
const bindOk = await bindNode(dcw.id)
|
|
108
|
+
check(`DCW 节点+孪生绑定 ${dev.name}`, !!dcw?.id && bindOk, `${dcw.id} ↔ twin ${twin.id}(${modelKey})`)
|
|
109
|
+
const recipe = (await api('POST', '/api/workshop/dcw/recipes', {
|
|
110
|
+
productId: product.id, name: `演示配方${lineIx}`,
|
|
111
|
+
params: [{ nodeId: dcw.id, value: dev.signals?.find(x => x.id === spItem.signal)?.value ?? (Number(spItem.min ?? 0) + Number(spItem.max ?? 300)) / 2, min: spItem.min ?? 0, max: spItem.max ?? 300 }],
|
|
112
|
+
})).data?.recipe
|
|
113
|
+
const st = (await api('POST', `/api/workshop/dcw/lines/${line.id}/start`, { recipeId: recipe.id }))
|
|
114
|
+
check(`产线开跑 ${line.name}`, !!st.data?.run?.id)
|
|
115
|
+
results.push({ dev, dcw, line, spItem })
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 标量 PV → 数采节点(挂到本线或第一条既有演示线)
|
|
119
|
+
for (const pv of pvItems) {
|
|
120
|
+
const cfg = pv.driverConfig
|
|
121
|
+
const daq = (await api('POST', '/api/workshop/daq', {
|
|
122
|
+
templateRef: 'daq-temp-tc', name: `演示·${dev.name}·${pv.signal}`,
|
|
123
|
+
driver: pv.driver, driverConfig: cfg,
|
|
124
|
+
unit: pv.unit ?? '', min: pv.min ?? 0, max: pv.max ?? 400,
|
|
125
|
+
lineId: hostLine?.id ?? '', intervalMs: 2000, publishIntervalMs: 0,
|
|
126
|
+
semantics: `模拟器设备「${dev.name}」的 ${pv.signal}(真实 ${pv.driver} 采样链路)`,
|
|
127
|
+
})).data?.node
|
|
128
|
+
if (!daq) {
|
|
129
|
+
check(`DAQ 节点 ${dev.name}·${pv.signal}`, false)
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
if (hostLine?.id) await bindDaq(daq.id)
|
|
133
|
+
results.push({ dev, daq, pvItem: pv })
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 卫星数采补绑定(无 SP 设备的 DAQ 挂到第一条演示线,否则不采样)
|
|
138
|
+
const firstLine = arr((await api('GET', '/api/workshop/dcw/lines')).data, 'lines').find(l => l.name.startsWith('演示线'))
|
|
139
|
+
if (firstLine) {
|
|
140
|
+
const allDaq = arr((await api('GET', '/api/workshop/daq')).data, 'nodes')
|
|
141
|
+
for (const n of allDaq.filter(x => x.name.startsWith('演示·') && !x.lineId)) {
|
|
142
|
+
await api('PATCH', `/api/workshop/daq/${n.id}`, { lineId: firstLine.id })
|
|
143
|
+
await bindDaqSoft(n.id)
|
|
144
|
+
}
|
|
145
|
+
async function bindDaqSoft(nodeId) {
|
|
146
|
+
const twin0 = twins[0]
|
|
147
|
+
if (twin0) await api('POST', `/api/workshop/daq/${nodeId}/bind`, { deviceId: twin0.id }).catch(() => {})
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// ── ④ 真连通验证:逐节点 test-driver ──
|
|
151
|
+
const allDcw = arr((await api('GET', '/api/workshop/dcw')).data, 'nodes').filter(n => n.name.startsWith('演示·'))
|
|
152
|
+
let connOk = 0
|
|
153
|
+
for (const n of allDcw.filter(x => x.driver !== 'mock')) {
|
|
154
|
+
const t = await api('POST', `/api/workshop/dcw/${n.id}/test`)
|
|
155
|
+
connOk += t.data?.test?.ok === true ? 1 : 0
|
|
156
|
+
}
|
|
157
|
+
check('逐节点真连通(test-driver)', connOk === allDcw.filter(x => x.driver !== 'mock').length, `${connOk}/${allDcw.filter(x => x.driver !== 'mock').length} 台真实驱动连通`)
|
|
158
|
+
|
|
159
|
+
// ── ⑤ 数采样本增长(活动批次内真实采样) ──
|
|
160
|
+
await sleep(9000)
|
|
161
|
+
const allDaq = arr((await api('GET', '/api/workshop/daq')).data, 'nodes').filter(n => n.name.startsWith('演示·'))
|
|
162
|
+
let sampling = 0
|
|
163
|
+
for (const n of allDaq) {
|
|
164
|
+
const s = await api('GET', `/api/workshop/daq/${n.id}/samples?limit=5`)
|
|
165
|
+
const pts = s.data?.points ?? []
|
|
166
|
+
sampling += pts.length > 0 ? 1 : 0
|
|
167
|
+
}
|
|
168
|
+
check('数采真实采样', sampling === allDaq.length, `${sampling}/${allDaq.length} 通道有样本(9s 窗)`)
|
|
169
|
+
|
|
170
|
+
console.log(`\n${failed === 0 ? '✅ 演示现场就绪' : `❌ ${failed} 项失败`}`)
|
|
171
|
+
process.exit(failed === 0 ? 0 : 1)
|
|
@@ -32,8 +32,10 @@ export default defineApiHandler(async (event) => {
|
|
|
32
32
|
dropped: state.dropped ?? 0,
|
|
33
33
|
samplesStored: state.samplesStored ?? 0,
|
|
34
34
|
},
|
|
35
|
-
// 驱动协议栈可用性(包缺失 → UI 显示"未安装")
|
|
35
|
+
// 驱动协议栈可用性(包缺失 → UI 显示"未安装";含插件驱动)
|
|
36
36
|
driverAvailable: await ctrl.driverAvailability(),
|
|
37
|
+
// 驱动目录(内置 + 协议插件自描述合并;前端添加节点下拉/参数表单同源消费)
|
|
38
|
+
drivers: await ctrl.driverCatalog(),
|
|
37
39
|
// 基础设施在线状态(降级 → 前端横幅 + 重连入口)
|
|
38
40
|
infra: daqInfraStatus(),
|
|
39
41
|
// 信号模板目录(内置 + 用户自定义;前端下拉/左轨/控制台同源消费)
|
|
@@ -6,6 +6,7 @@ import { filterByLine, isPrivilegedRole, visibleLineIds } from '@/server/service
|
|
|
6
6
|
import { defineApiHandler } from '@/server/utils/response'
|
|
7
7
|
import { bindDcwBroadcast, getDcwController } from '@/server/services/workshop/dcw/dcw-controller'
|
|
8
8
|
import { listDcwTemplates } from '@/server/services/workshop/dcw/dcw-templates'
|
|
9
|
+
import { dcwDriverCatalog } from '@/server/services/workshop/dcw/drivers'
|
|
9
10
|
import { broadcastSceneEvent } from '@/server/services/workshop/scene-events'
|
|
10
11
|
|
|
11
12
|
export default defineApiHandler(async (event) => {
|
|
@@ -28,6 +29,8 @@ export default defineApiHandler(async (event) => {
|
|
|
28
29
|
/** 工艺参数映射面(用户/Agent 的参数语义读写面;PLC 寻址细节不透出) */
|
|
29
30
|
params: visible ? ctrl.listParamViews().filter(p => inLine(p.lineId)) : ctrl.listParamViews(),
|
|
30
31
|
templates: listDcwTemplates(),
|
|
32
|
+
// 写驱动目录(内置 + 协议插件自描述合并;前端写控节点向导同源消费)
|
|
33
|
+
drivers: await dcwDriverCatalog(),
|
|
31
34
|
recipes: visible ? ctrl.listRecipes().filter(r => inLine(r.lineId)) : ctrl.listRecipes(),
|
|
32
35
|
runs: visible ? ctrl.listRuns().filter(r => inLine(r.lineId)) : ctrl.listRuns(),
|
|
33
36
|
history: visible
|
|
@@ -9,13 +9,15 @@ import { existsSync } from 'node:fs'
|
|
|
9
9
|
import { dirname, resolve } from 'node:path'
|
|
10
10
|
|
|
11
11
|
// packageRoot = 本包根。nitro 打包后 import.meta.url 位于 .output/server/chunks/ 内,
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
// 且 chunk 相对包根的层级随 nitro 版本变化(如 chunks/build/xxx.mjs 比源码布局深一层),
|
|
13
|
+
// 固定 2/3 级上溯在**全局安装布局**(node_modules/agentworkshop/)下探测不到包根,
|
|
14
|
+
// builtin 插件会全部失踪。改为从 chunk 目录逐级上溯(上限 8 级 ≫ 任何已知布局),
|
|
15
|
+
// 以「server/plugins-builtin 真实存在」为命中判据;最后兜底 cwd(源码运行)。
|
|
16
|
+
const candidates: string[] = []
|
|
17
|
+
for (let dir = dirname(fileURLToPath(import.meta.url)), i = 0; i < 8; i++, dir = dirname(dir)) {
|
|
18
|
+
candidates.push(dir)
|
|
19
|
+
}
|
|
20
|
+
candidates.push(process.cwd())
|
|
19
21
|
const packageRoot = candidates.find(r => existsSync(resolve(r, 'server', 'plugins-builtin'))) ?? process.cwd()
|
|
20
22
|
|
|
21
23
|
export default defineNitroPlugin((nitroApp) => {
|