agentworkshop 0.7.18 → 0.7.20
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/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/781a7557-9009-4289-ab59-7905c4f96825.json +1 -0
- package/.output/server/chunks/_/nitro.mjs +650 -650
- package/.output/server/package.json +1 -1
- package/package.json +1 -1
- package/scripts/_dbg-live-line-e2e.mjs +279 -0
- package/scripts/_dbg-mqtt-publisher.mjs +17 -0
- package/scripts/_dbg-multiharness-live-e2e.mjs +254 -0
- package/scripts/_dbg-pg-tcp-probe.cjs +27 -0
- package/server/data/daqs.json +519 -100
- package/server/data/dcw-lines.json +28 -0
- package/server/data/dcw-products.json +28 -0
- package/server/data/dcw-recipes.json +138 -0
- package/server/data/dcw-rollback.json +1619 -9
- package/server/data/dcw-runs.json +249 -189
- package/server/data/dcw-writes.json +600 -0
- package/server/data/dcws.json +909 -466
- package/server/data/line-runs.json +55 -15
- package/.output/public/_nuxt/builds/meta/253c093e-3336-4451-9f11-08061bf22f01.json +0 -1
package/package.json
CHANGED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模拟真实产线全栈 E2E(生产实例 :3001,真实协议:Modbus TCP/RTU、MQTT、OPC UA、HTTP)
|
|
3
|
+
*
|
|
4
|
+
* S1 夹具:产线+产品+配方+5 数采节点(modbus-tcp/rtu/mqtt/opcua/http)+5 数控节点
|
|
5
|
+
* S2 协议连通:逐节点驱动 test(真实连接)
|
|
6
|
+
* S3 数采:开跑 → 采样流入(mock 之外的 5 协议节点值变化)+ Timescale 落库
|
|
7
|
+
* S4 数控:REST 下发 modbus-tcp/rtu/opcua/http/mqtt 五路(真实协议写+回读/路由捕获)
|
|
8
|
+
* S5 Agent 闭环:绑定 → 任务(dcw_control→daq_query→dcw_judge)→ CLOSEDLOOP-OK
|
|
9
|
+
* S6 HITL:manual 绑定的 OPC UA 节点 → 待审批 → 批准 → 真实写入
|
|
10
|
+
* S7 Recipe:下发(PLC=目标)/recipe_update/recipe_rollback/版本史/参数账本回退
|
|
11
|
+
*
|
|
12
|
+
* 用法:NO_PROXY='127.0.0.1,localhost' node scripts/_dbg-live-line-e2e.mjs [base] [tag]
|
|
13
|
+
*/
|
|
14
|
+
const BASE = process.argv[2] ?? 'http://127.0.0.1:3001'
|
|
15
|
+
const TAG = process.argv[3] ?? `lv${Math.random().toString(36).slice(2, 6)}`
|
|
16
|
+
const { createRequire } = await import('node:module')
|
|
17
|
+
const reqMqtt = createRequire(import.meta.url)
|
|
18
|
+
let pass = 0
|
|
19
|
+
let fail = 0
|
|
20
|
+
const ok = (cond, name, extra = '') => {
|
|
21
|
+
if (cond) {
|
|
22
|
+
pass++
|
|
23
|
+
console.log(` ✓ ${name}`)
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
fail++
|
|
27
|
+
console.log(` ✗ ${name} ${extra}`)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
31
|
+
const api = async (method, path, { body, token } = {}) => {
|
|
32
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
33
|
+
method,
|
|
34
|
+
headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
35
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
36
|
+
signal: AbortSignal.timeout(60_000),
|
|
37
|
+
})
|
|
38
|
+
return { status: res.status, ...(await res.json().catch(() => ({}))) }
|
|
39
|
+
}
|
|
40
|
+
const invoke = (token, agentId, tool, args) => api('POST', '/api/workshop/agent-tools/invoke', { body: { agentId, tool, args }, token })
|
|
41
|
+
|
|
42
|
+
async function main() {
|
|
43
|
+
console.log(`━━━ 模拟真实产线全栈 E2E @ ${BASE} (tag=${TAG}) ━━━`)
|
|
44
|
+
|
|
45
|
+
// ════ S0 登录 ════
|
|
46
|
+
const login = await api('POST', '/api/users/login', { body: { email: 'admin@awshop.local', password: 'admin123' } })
|
|
47
|
+
if (login.data?.token === undefined) {
|
|
48
|
+
// 隔离库回退:admin 用户名
|
|
49
|
+
const alt = await api('POST', '/api/users/login', { body: { email: 'admin', password: 'admin123' } })
|
|
50
|
+
login.data = alt.data
|
|
51
|
+
}
|
|
52
|
+
const token = login.data?.token
|
|
53
|
+
ok(Boolean(token), 'admin 登录')
|
|
54
|
+
if (!token) process.exit(1)
|
|
55
|
+
|
|
56
|
+
// ════ S1 夹具 ════
|
|
57
|
+
const line = (await api('POST', '/api/workshop/dcw/lines', { body: { name: `模拟产线-${TAG}` }, token })).data?.line
|
|
58
|
+
const product = (await api('POST', '/api/workshop/dcw/products', { body: { name: `模拟产品-${TAG}`, lineId: line.id }, token })).data?.product
|
|
59
|
+
ok(Boolean(line?.id && product?.id), `产线/产品创建(${line.id.slice(0, 8)})`)
|
|
60
|
+
|
|
61
|
+
// 5 数采节点(5 协议)
|
|
62
|
+
const daqDefs = [
|
|
63
|
+
{ key: 'modbus-tcp', name: `进料压力-MBTCP`, driverConfig: { host: '127.0.0.1', port: 1502, unitId: 1, register: 40001, registerType: 'holding', dataType: 'float32', scale: 1, byteOrder: 'big' }, template: 'pressure-tx' },
|
|
64
|
+
{ key: 'modbus-rtu', name: `RTU 温度`, driverConfig: { host: '127.0.0.1', port: 15030, unitId: 1, register: 40001, registerType: 'holding', dataType: 'float32', scale: 1, byteOrder: 'big' }, template: 'temp-tc' },
|
|
65
|
+
{ key: 'opcua', name: `OPC 炉温`, driverConfig: { endpoint: 'opc.tcp://127.0.0.1:4840', nodeId: 'ns=2;s=AW.Temp' }, template: 'temp-tc' },
|
|
66
|
+
{ key: 'mqtt', name: `MQTT 均温`, driverConfig: { host: '127.0.0.1', port: 1883, topic: 'aw/sim/temp', jsonPath: 'data.temp' }, template: 'temp-tc' },
|
|
67
|
+
{ key: 'http', name: `HTTP 品质值`, driverConfig: { url: 'http://127.0.0.1:1889/api/value', jsonPath: 'data.value' }, template: 'temp-tc' },
|
|
68
|
+
]
|
|
69
|
+
const daqNodes = {}
|
|
70
|
+
for (const d of daqDefs) {
|
|
71
|
+
const test = await api('POST', '/api/workshop/daq/test-driver', { body: { driver: d.key, driverConfig: d.driverConfig }, token })
|
|
72
|
+
const node = (await api('POST', '/api/workshop/daq', {
|
|
73
|
+
body: { name: `${d.name}-${TAG}`, templateRef: d.template, driver: d.key, driverConfig: d.driverConfig, unit: d.key === 'modbus-tcp' ? 'MPa' : '℃', min: 0, max: 200, intervalMs: 2000, lineId: line.id },
|
|
74
|
+
token,
|
|
75
|
+
})).data?.node
|
|
76
|
+
daqNodes[d.key] = node
|
|
77
|
+
const testOk = test.data?.test?.ok !== false
|
|
78
|
+
ok(Boolean(node?.id), `[daq] ${d.key} 创建(test=${testOk ? '连通' : (test.data?.test?.message ?? '失败')})`, test.data?.test?.message)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 5 数控节点(5 协议)
|
|
82
|
+
const dcwDefs = [
|
|
83
|
+
{ key: 'modbus-tcp', name: `压力设定-MBTCP`, driverConfig: { host: '127.0.0.1', port: 1502, unitId: 1, register: 40021, dataType: 'float32', byteOrder: 'big' }, unit: 'MPa', min: 0, max: 5 },
|
|
84
|
+
{ key: 'modbus-rtu', name: `RTU 开度`, driverConfig: { host: '127.0.0.1', port: 15030, unitId: 1, register: 40021, dataType: 'int16', byteOrder: 'big' }, unit: '%', min: 0, max: 10000 },
|
|
85
|
+
{ key: 'opcua', name: `OPC 炉温设定`, driverConfig: { endpoint: 'opc.tcp://127.0.0.1:4840', nodeId: 'ns=2;s=AW.SetTemp' }, unit: '℃', min: 0, max: 200 },
|
|
86
|
+
{ key: 'mqtt', name: `MQTT 下位机设定`, driverConfig: { host: '127.0.0.1', port: 1883, topic: `aw/sim/setpoint/${TAG}`, jsonKey: 'value' }, unit: '℃', min: 0, max: 200 },
|
|
87
|
+
{ key: 'http', name: `HTTP 加热设定`, driverConfig: { url: 'http://127.0.0.1:1889/api/setpoint', bodyKey: 'value' }, unit: '℃', min: 0, max: 200 },
|
|
88
|
+
]
|
|
89
|
+
const dcwNodes = {}
|
|
90
|
+
for (const d of dcwDefs) {
|
|
91
|
+
const node = (await api('POST', '/api/workshop/dcw', {
|
|
92
|
+
body: { name: `${d.name}-${TAG}`, templateRef: 'temp-sp', driver: d.key, driverConfig: d.driverConfig, unit: d.unit, min: d.min, max: d.max, lineId: line.id },
|
|
93
|
+
token,
|
|
94
|
+
})).data?.node
|
|
95
|
+
dcwNodes[d.key] = node
|
|
96
|
+
ok(Boolean(node?.id), `[dcw] ${d.key} 创建`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 配方:2 个真实协议参数
|
|
100
|
+
const recipe = (await api('POST', '/api/workshop/dcw/recipes', {
|
|
101
|
+
body: { name: `工艺A-${TAG}`, productId: product.id, params: [
|
|
102
|
+
{ nodeId: dcwNodes['modbus-tcp'].id, value: 0.9, min: 0.6, max: 1.1 },
|
|
103
|
+
{ nodeId: dcwNodes['modbus-rtu'].id, value: 500 },
|
|
104
|
+
] },
|
|
105
|
+
token,
|
|
106
|
+
})).data?.recipe
|
|
107
|
+
ok(Boolean(recipe?.id), `配方创建(${recipe?.id})`)
|
|
108
|
+
|
|
109
|
+
// ════ S2 数采协议连通(节点级 test)════
|
|
110
|
+
for (const [k, n] of Object.entries(daqNodes)) {
|
|
111
|
+
const t = await api('POST', `/api/workshop/daq/${n.id}/test`, { body: {}, token })
|
|
112
|
+
ok(t.data?.ok === true || t.data?.ok === undefined || t.status === 200, `[daq] ${k} test 连通`, JSON.stringify(t.data ?? t).slice(0, 90))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ════ S3 数采流入(开跑 → 采样 + Timescale)════
|
|
116
|
+
const producedBefore = (await api('GET', '/api/workshop/daq', { token })).data?.meta?.produced ?? 0
|
|
117
|
+
const start = await api('POST', `/api/workshop/dcw/lines/${line.id}/start`, { body: { recipeId: recipe.id }, token })
|
|
118
|
+
ok(start.status === 200, '产线开跑(绑定配方批次)', start.message)
|
|
119
|
+
await sleep(9000)
|
|
120
|
+
const daqSnap = (await api('GET', '/api/workshop/daq', { token })).data
|
|
121
|
+
const liveNodes = daqSnap.nodes.filter(n => Object.values(daqNodes).some(x => x?.id === n.id))
|
|
122
|
+
const changing = liveNodes.filter(n => n.value != null)
|
|
123
|
+
ok(changing.length >= 4, `数采流入:5 协议节点中 ${changing.length} 个有实时值(${changing.map(n => `${n.name}=${n.value}`).join('; ').slice(0, 140)})`)
|
|
124
|
+
const prodAfter = daqSnap.meta?.produced ?? 0
|
|
125
|
+
ok(prodAfter > producedBefore, `管线 produced 增长(${producedBefore} → ${prodAfter})`)
|
|
126
|
+
|
|
127
|
+
// ════ S4 数控五协议下发 ════
|
|
128
|
+
const w1 = await api('POST', `/api/workshop/dcw/${dcwNodes['modbus-tcp'].id}/write`, { body: { value: 0.95 }, token })
|
|
129
|
+
ok(w1.data?.outcome?.ok === true || w1.data?.ok !== false, `modbus-tcp 写 0.95(回读 ${w1.data?.outcome?.readback ?? w1.data?.readback ?? '?'})`, JSON.stringify(w1.data ?? w1).slice(0, 120))
|
|
130
|
+
const w2 = await api('POST', `/api/workshop/dcw/${dcwNodes['modbus-rtu'].id}/write`, { body: { value: 500 }, token })
|
|
131
|
+
ok(w2.data?.outcome?.ok === true || w2.data?.ok !== false, `modbus-rtu 写 500(int16,回读 ${w2.data?.outcome?.readback ?? w2.data?.readback ?? '?'})`, JSON.stringify(w2.data ?? w2).slice(0, 120))
|
|
132
|
+
const w3 = await api('POST', `/api/workshop/dcw/${dcwNodes['opcua'].id}/write`, { body: { value: 88 }, token })
|
|
133
|
+
ok(w3.data?.outcome?.ok === true, `opcua 写 88(回读 ${w3.data?.outcome?.readback ?? '?'})`, JSON.stringify(w3.data ?? w3).slice(0, 120))
|
|
134
|
+
const w4 = await api('POST', `/api/workshop/dcw/${dcwNodes['http'].id}/write`, { body: { value: 66.6 }, token })
|
|
135
|
+
ok(w4.data?.outcome?.ok === true, `http 写 66.6(POST /api/setpoint,回读 ${w4.data?.outcome?.readback ?? '?'})`, JSON.stringify(w4.data ?? w4).slice(0, 120))
|
|
136
|
+
|
|
137
|
+
// mqtt 下发:订阅容器内 mosquitto?此处 1883 是 proto-sim broker —— 用 node 订阅捕获
|
|
138
|
+
const captured = await captureMqtt(`aw/sim/setpoint/${TAG}`, async () => {
|
|
139
|
+
await api('POST', `/api/workshop/dcw/${dcwNodes['mqtt'].id}/write`, { body: { value: 55.5 }, token })
|
|
140
|
+
})
|
|
141
|
+
ok(captured.includes('55.5'), `mqtt 下发路由捕获(topic=aw/sim/setpoint/${TAG} payload 含 55.5)`, captured.slice(0, 80))
|
|
142
|
+
|
|
143
|
+
// ════ S5 Agent 闭环(真实协议节点)════
|
|
144
|
+
const ch = (await api('POST', '/api/workshop/channels', {
|
|
145
|
+
body: { name: `live-${TAG}`, leadAgent: { name: `lead-${TAG}`, harness: 'mock', config: { delayMs: 60 } } },
|
|
146
|
+
token,
|
|
147
|
+
})).data
|
|
148
|
+
const tpl = await api('POST', '/api/workshop/agents', {
|
|
149
|
+
body: { name: `op-${TAG}`, harness: 'omp', config: { provider: 'zhipu-coding-plan', model: 'glm-5.3-flash', systemPromptPrefix: '你是产线操作员:严格按步骤执行,完成后立即调用 complete_task。' } },
|
|
150
|
+
token,
|
|
151
|
+
})
|
|
152
|
+
const join = await api('POST', `/api/workshop/channels/${ch.channelId}/agents`, { body: { agentId: tpl.data.id, role: 'worker' }, token })
|
|
153
|
+
const instId = join.data?.id
|
|
154
|
+
await api('POST', '/api/workshop/agent-tools/bindings', { body: { agentId: instId, nodeId: dcwNodes['modbus-tcp'].id, kind: 'dcw', mode: 'auto' }, token })
|
|
155
|
+
await api('POST', '/api/workshop/agent-tools/bindings', { body: { agentId: instId, nodeId: daqNodes['modbus-tcp'].id, kind: 'daq', mode: 'auto' }, token })
|
|
156
|
+
// HITL:opcua 设定节点 manual 模式
|
|
157
|
+
await api('POST', '/api/workshop/agent-tools/bindings', { body: { agentId: instId, nodeId: dcwNodes['opcua'].id, kind: 'dcw', mode: 'manual' }, token })
|
|
158
|
+
ok(Boolean(instId), 'Agent 入队 + 三节点绑定(auto×2 + manual×1)')
|
|
159
|
+
|
|
160
|
+
// 闭环任务
|
|
161
|
+
const t1 = await api('POST', `/api/workshop/channels/${ch.channelId}/tasks`, {
|
|
162
|
+
body: {
|
|
163
|
+
title: `closedloop-${TAG}`,
|
|
164
|
+
parts: [{ text: `闭环调优(严格按步骤):
|
|
165
|
+
1. dcw_control(node_id="${dcwNodes['modbus-tcp'].id}", value=0.95, hypothesis="提压验证") 下发压力设定;
|
|
166
|
+
2. 等待 3 秒后 daq_query(node_id="${daqNodes['modbus-tcp'].id}") 查看进料压力响应;
|
|
167
|
+
3. 基于数采证据 dcw_judge:对刚才下发的优化记录落判定 keep(附一句证据);
|
|
168
|
+
4. 最终交付原样包含一行标记 CLOSEDLOOP-OK,并附压力设定值与采样值各一个。
|
|
169
|
+
完成后调用 complete_task。` }],
|
|
170
|
+
assigneeId: instId,
|
|
171
|
+
},
|
|
172
|
+
token,
|
|
173
|
+
})
|
|
174
|
+
const task1 = t1.data?.task?.id ?? t1.data?.id
|
|
175
|
+
const st1 = await pollTask(token, task1, 12 * 60_000)
|
|
176
|
+
ok(st1 === 'COMPLETED', `[闭环] 任务 COMPLETED(state=${st1})`)
|
|
177
|
+
const blob1 = await gatherTaskBlob(token, ch.channelId, task1)
|
|
178
|
+
ok(blob1.includes('CLOSEDLOOP-OK'), '[闭环] 交付含 CLOSEDLOOP-OK(真实协议下发+数采证据+判定)')
|
|
179
|
+
|
|
180
|
+
// ════ S6 HITL(manual 绑定 → invoke 挂起 → 审批 → 真实写入)════
|
|
181
|
+
// HITL 语义:invoke 挂起等待裁决(超时 300s),因此后台发起、轮询待办、批准后收结果
|
|
182
|
+
const hitlPromise = invoke(token, instId, 'dcw_control', { node_id: dcwNodes['opcua'].id, value: 92, task_id: 'hitl-e2e' }).catch(err => ({ data: { result: { text: `invoke err: ${err.message}`, isError: true } } }))
|
|
183
|
+
let pending = null
|
|
184
|
+
for (let i = 0; i < 10 && !pending; i++) {
|
|
185
|
+
await sleep(1500)
|
|
186
|
+
const pend = (await api('GET', '/api/workshop/agent-tools/approvals', { token })).data?.approvals ?? []
|
|
187
|
+
pending = pend.find(p => p.agentId === instId) ?? null
|
|
188
|
+
}
|
|
189
|
+
ok(Boolean(pending?.id), `[HITL] 产生待审批(${pending?.id ?? '无'})`)
|
|
190
|
+
if (pending?.id) {
|
|
191
|
+
const dec = await api('POST', `/api/workshop/agent-tools/approvals/${pending.id}/decide`, { body: { approved: true, comment: 'E2E 批准' }, token })
|
|
192
|
+
ok(dec.status === 200, '[HITL] 管理员批准')
|
|
193
|
+
const hitl = await hitlPromise
|
|
194
|
+
ok(hitl.data?.result?.isError !== true, '[HITL] Agent 收到批准结果并执行', (hitl.data?.result?.text ?? '').slice(0, 90))
|
|
195
|
+
await sleep(1500)
|
|
196
|
+
const rd = await api('POST', `/api/workshop/dcw/${dcwNodes['opcua'].id}/read`, { body: {}, token })
|
|
197
|
+
ok(String(rd.data?.read?.value ?? '').startsWith('92'), `[HITL] 批准后真实写入生效(OPC UA SetTemp 回读 ${rd.data?.read?.value})`, JSON.stringify(rd.data ?? rd).slice(0, 90))
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
await hitlPromise.catch(() => {})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ════ S7 Recipe 全生命周期(真实协议下发)════
|
|
204
|
+
const upd = await invoke(token, instId, 'recipe_update', { recipe_id: recipe.id, params: [{ node_id: dcwNodes['modbus-tcp'].id, value: 0.98 }], reason: '闭环验证:压力窗口内稳定' })
|
|
205
|
+
ok(upd.data?.result?.text?.includes('v2'), '[recipe] Agent recipe_update → v2', upd.data?.result?.text?.slice(0, 90))
|
|
206
|
+
const rv = await api('POST', `/api/workshop/dcw/recipes/${recipe.id}/revert`, { body: { version: 1, reason: 'E2E 回退到初版' }, token })
|
|
207
|
+
ok(rv.data?.recipe?.version === 3, '[recipe] 界面回退 → v3')
|
|
208
|
+
const jl = await invoke(token, instId, 'dcw_journal', { node_id: dcwNodes['modbus-tcp'].id })
|
|
209
|
+
ok(jl.data?.result?.text?.includes('优化记录') || jl.data?.result?.text?.includes('锚'), '[journal] dcw_journal 参数账本可查')
|
|
210
|
+
// 节点级参数回退:回退压力设定到上一稳定锚
|
|
211
|
+
const rb = await invoke(token, instId, 'dcw_rollback', { node_id: dcwNodes['modbus-tcp'].id })
|
|
212
|
+
const rbOk = rb.data?.result?.text?.includes('回退') && !rb.data?.result?.isError
|
|
213
|
+
const rbSkip = (rb.data?.result?.text ?? '').includes('冷却') || (rb.data?.result?.text ?? '').includes('无可回退')
|
|
214
|
+
ok(rbOk || rbSkip, `[journal] dcw_rollback 执行(${rb.data?.result?.text?.slice(0, 70) ?? '∅'})`)
|
|
215
|
+
|
|
216
|
+
// ════ 清理 ════
|
|
217
|
+
await api('POST', `/api/workshop/dcw/lines/${line.id}/stop`, { body: {}, token }).catch(() => {})
|
|
218
|
+
await api('DELETE', `/api/workshop/channels/${ch.channelId}?purge=1`, { token }).catch(() => {})
|
|
219
|
+
await api('DELETE', `/api/workshop/agents/${tpl.data.id}`, { token }).catch(() => {})
|
|
220
|
+
await api('DELETE', `/api/workshop/dcw/recipes/${recipe.id}`, { token }).catch(() => {})
|
|
221
|
+
await api('DELETE', `/api/workshop/dcw/products/${product.id}`, { token }).catch(() => {})
|
|
222
|
+
for (const n of Object.values(dcwNodes)) await api('DELETE', `/api/workshop/dcw/${n.id}`, { token }).catch(() => {})
|
|
223
|
+
for (const n of Object.values(daqNodes)) await api('DELETE', `/api/workshop/daq/${n.id}`, { token }).catch(() => {})
|
|
224
|
+
await api('DELETE', `/api/workshop/dcw/lines/${line.id}`, { token }).catch(() => {})
|
|
225
|
+
|
|
226
|
+
console.log(`\n━━━ LiveLine E2E: ${pass} passed / ${fail} failed ━━━`)
|
|
227
|
+
process.exit(fail === 0 ? 0 : 1)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function pollTask(token, id, timeoutMs) {
|
|
231
|
+
const deadline = Date.now() + timeoutMs
|
|
232
|
+
let state = ''
|
|
233
|
+
while (Date.now() < deadline) {
|
|
234
|
+
for (let i = 0; i < 3; i++) {
|
|
235
|
+
try {
|
|
236
|
+
const me = await api('GET', `/api/workshop/tasks/${id}`, { token })
|
|
237
|
+
state = me.data?.state ?? ''
|
|
238
|
+
break
|
|
239
|
+
}
|
|
240
|
+
catch { await sleep(3000) }
|
|
241
|
+
}
|
|
242
|
+
if (['COMPLETED', 'FAILED', 'CANCELED'].includes(state)) return state
|
|
243
|
+
await sleep(6000)
|
|
244
|
+
}
|
|
245
|
+
return state || 'RUNNING'
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function gatherTaskBlob(token, channelId, taskId) {
|
|
249
|
+
const taskBlob = JSON.stringify((await api('GET', `/api/workshop/tasks/${taskId}`, { token })).data ?? {})
|
|
250
|
+
const msgBlob = JSON.stringify((await api('GET', `/api/workshop/channels/${channelId}/messages?limit=200`, { token })).data ?? {})
|
|
251
|
+
const evBlob = JSON.stringify((await api('GET', `/api/workshop/channels/${channelId}/events?limit=400`, { token })).data ?? {})
|
|
252
|
+
return taskBlob + msgBlob + evBlob
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** 订阅 MQTT 主题并执行动作,断言捕获 payload(真实 broker = 127.0.0.1:1883) */
|
|
256
|
+
async function captureMqtt(topic, action) {
|
|
257
|
+
const mqtt = createMqttLib()
|
|
258
|
+
return await new Promise((resolve) => {
|
|
259
|
+
const client = mqtt.connect('mqtt://127.0.0.1:1883')
|
|
260
|
+
let got = ''
|
|
261
|
+
const done = () => { try { client.end(true) } catch {} ; resolve(got) }
|
|
262
|
+
client.on('connect', async () => {
|
|
263
|
+
client.subscribe(topic, () => {
|
|
264
|
+
setTimeout(action, 300)
|
|
265
|
+
})
|
|
266
|
+
setTimeout(() => done(), 9000)
|
|
267
|
+
})
|
|
268
|
+
client.on('message', (t, payload) => { got = `${t} ${payload.toString()}` })
|
|
269
|
+
setTimeout(() => done(), 12_000)
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
function createMqttLib() {
|
|
273
|
+
return reqMqtt('mqtt')
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
main().catch((err) => {
|
|
277
|
+
console.error('E2E 异常:', err)
|
|
278
|
+
process.exit(1)
|
|
279
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MQTT 模拟数据发布器:向真实 mosquitto(1883)每 2s 发布 aw/sim/temp = {"data":{"temp":…}},
|
|
3
|
+
* 供 daq mqtt 驱动节点订阅采样(与 dev-protocol-simulators 的载荷形状一致)。
|
|
4
|
+
* 用法:node scripts/_dbg-mqtt-publisher.mjs (前台常驻;SIGINT 退出)
|
|
5
|
+
*/
|
|
6
|
+
import { createRequire } from 'node:module'
|
|
7
|
+
const req = createRequire(import.meta.url)
|
|
8
|
+
const mqtt = req('mqtt')
|
|
9
|
+
|
|
10
|
+
const client = mqtt.connect('mqtt://127.0.0.1:1883', { reconnectPeriod: 3000 })
|
|
11
|
+
let temp = 51.2
|
|
12
|
+
client.on('connect', () => console.log('[mqtt-pub] 已连接 mosquitto 1883,开始发布 aw/sim/temp'))
|
|
13
|
+
client.on('error', (e) => console.error('[mqtt-pub] error:', e.message))
|
|
14
|
+
setInterval(() => {
|
|
15
|
+
temp = Number(Math.max(40, Math.min(60, temp + (Math.random() - 0.48) * 0.6)).toFixed(2))
|
|
16
|
+
client.publish('aw/sim/temp', JSON.stringify({ data: { temp } }), { qos: 0 })
|
|
17
|
+
}, 2000)
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 多 Harness 真实端到端 E2E(生产实例 :3001,真实 Modbus TCP 模拟器)
|
|
3
|
+
*
|
|
4
|
+
* 四引擎并行,各自独立 Channel/Worker/绑定,场景互不重叠:
|
|
5
|
+
* omp → 闭环控制:dcw_control → daq_query → dcw_judge keep (OMP-CLOSEDLOOP-OK)
|
|
6
|
+
* codex → 数据控制:dcw_control 写真实寄存器 → dcw_journal 账本 (CODEX-WRITE-OK)
|
|
7
|
+
* dsh → 数据采集:daq_query 真实采样 + line_context 归属 (DSH-DAQ-OK)
|
|
8
|
+
* opencode → Recipe 写入回退:recipe_update → recipe_rollback (OC-RECIPE-OK)
|
|
9
|
+
* 服务端状态断言:codex 写后 PLC 回读=目标;opencode 版本净增 2;产线启停生命周期。
|
|
10
|
+
*
|
|
11
|
+
* 用法:NO_PROXY='127.0.0.1,localhost' node scripts/_dbg-multiharness-live-e2e.mjs [base]
|
|
12
|
+
*/
|
|
13
|
+
const BASE = process.argv[2] ?? 'http://127.0.0.1:3001'
|
|
14
|
+
const TAG = `mh${Math.random().toString(36).slice(2, 6)}`
|
|
15
|
+
let pass = 0
|
|
16
|
+
let fail = 0
|
|
17
|
+
const ok = (cond, name, extra = '') => {
|
|
18
|
+
if (cond) { pass++; console.log(` ✓ ${name}`) }
|
|
19
|
+
else { fail++; console.log(` ✗ ${name} ${extra}`) }
|
|
20
|
+
}
|
|
21
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
22
|
+
const { createRequire } = await import('node:module')
|
|
23
|
+
|
|
24
|
+
const api = async (method, path, { body, token, timeoutMs } = {}) => {
|
|
25
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
26
|
+
method,
|
|
27
|
+
headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
28
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
29
|
+
signal: AbortSignal.timeout(timeoutMs ?? 30_000),
|
|
30
|
+
})
|
|
31
|
+
return { status: res.status, ...(await res.json().catch(() => ({}))) }
|
|
32
|
+
}
|
|
33
|
+
const invoke = (token, agentId, tool, args) => api('POST', '/api/workshop/agent-tools/invoke', { body: { agentId, tool, args }, token })
|
|
34
|
+
|
|
35
|
+
const ENGINES = [
|
|
36
|
+
{ key: 'omp', harness: 'omp', cfg: { provider: 'zhipu-coding-plan', model: 'glm-5.3-flash' } },
|
|
37
|
+
{ key: 'codex', harness: 'codex', cfg: { model: 'glm-5.3-flash', approvalPolicy: 'never' } },
|
|
38
|
+
{ key: 'dsh', harness: 'dsh', cfg: { provider: 'ustc', model: 'glm-5.3-flash' } },
|
|
39
|
+
{ key: 'opencode', harness: 'opencode', cfg: { model: 'zhipuai-coding-plan/glm-5.3-flash' } },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
async function pollTask(token, id, timeoutMs) {
|
|
43
|
+
const deadline = Date.now() + timeoutMs
|
|
44
|
+
let state = ''
|
|
45
|
+
while (Date.now() < deadline) {
|
|
46
|
+
for (let i = 0; i < 3; i++) {
|
|
47
|
+
try {
|
|
48
|
+
const me = await api('GET', `/api/workshop/tasks/${id}`, { token })
|
|
49
|
+
state = me.data?.state ?? ''
|
|
50
|
+
break
|
|
51
|
+
}
|
|
52
|
+
catch { await sleep(3000) }
|
|
53
|
+
}
|
|
54
|
+
if (['COMPLETED', 'FAILED', 'CANCELED'].includes(state)) return state
|
|
55
|
+
await sleep(6000)
|
|
56
|
+
}
|
|
57
|
+
return state || 'RUNNING'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function gatherTaskBlob(token, channelId, taskId) {
|
|
61
|
+
const parts = await Promise.all([
|
|
62
|
+
api('GET', `/api/workshop/tasks/${taskId}`, { token }),
|
|
63
|
+
api('GET', `/api/workshop/channels/${channelId}/messages?limit=200`, { token }),
|
|
64
|
+
api('GET', `/api/workshop/channels/${channelId}/events?limit=400`, { token }),
|
|
65
|
+
])
|
|
66
|
+
return parts.map(p => JSON.stringify(p.data ?? {})).join('')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function main() {
|
|
70
|
+
console.log(`━━━ 多 Harness 真实 E2E @ ${BASE} (tag=${TAG}) ━━━`)
|
|
71
|
+
// 清理上一轮遗留(异常中断未清理的 mh-* 产线)
|
|
72
|
+
try {
|
|
73
|
+
const d = (await api('GET', '/api/workshop/dcw', { token })).data
|
|
74
|
+
for (const l of d.lines.filter(x => x.name.startsWith('mh-line-'))) {
|
|
75
|
+
for (const r of d.recipes.filter(x => x.lineId === l.id)) await api('DELETE', `/api/workshop/dcw/recipes/${r.id}`, { token })
|
|
76
|
+
for (const p of d.products.filter(x => x.lineId === l.id)) await api('DELETE', `/api/workshop/dcw/products/${p.id}`, { token })
|
|
77
|
+
for (const n of d.nodes.filter(x => x.lineId === l.id)) await api('DELETE', `/api/workshop/dcw/${n.id}`, { token })
|
|
78
|
+
for (const n of (await api('GET', '/api/workshop/daq', { token })).data.nodes.filter(x => x.lineId === l.id)) await api('DELETE', `/api/workshop/daq/${n.id}`, { token })
|
|
79
|
+
await api('DELETE', `/api/workshop/dcw/lines/${l.id}`, { token })
|
|
80
|
+
}
|
|
81
|
+
for (const c of (await api('GET', '/api/workshop/channels', { token })).data.filter(x => x.name.startsWith('mh-'))) {
|
|
82
|
+
await api('DELETE', `/api/workshop/channels/${c.id}?purge=1`, { token })
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch { /* 首轮无遗留 */ }
|
|
86
|
+
|
|
87
|
+
const login = await api('POST', '/api/users/login', { body: { email: 'admin@awshop.local', password: 'admin123' } })
|
|
88
|
+
const token = login.data?.token
|
|
89
|
+
ok(Boolean(token), 'admin 登录')
|
|
90
|
+
if (!token) process.exit(1)
|
|
91
|
+
|
|
92
|
+
// ════ 夹具:产线 + 节点 + 产品/配方 ════
|
|
93
|
+
const line = (await api('POST', '/api/workshop/dcw/lines', { body: { name: `mh-line-${TAG}` }, token })).data?.line
|
|
94
|
+
const nodeA = (await api('POST', '/api/workshop/dcw', {
|
|
95
|
+
body: { name: `mh-press-${TAG}`, templateRef: 'temp-sp', driver: 'modbus-tcp', driverConfig: { host: '127.0.0.1', port: 1502, unitId: 1, register: 40021, dataType: 'float32', byteOrder: 'big' }, unit: 'MPa', min: 0, max: 5, lineId: line.id },
|
|
96
|
+
token,
|
|
97
|
+
})).data?.node
|
|
98
|
+
const daq = (await api('POST', '/api/workshop/daq', {
|
|
99
|
+
body: { name: `mh-daq-${TAG}`, templateRef: 'pressure-tx', driver: 'modbus-tcp', driverConfig: { host: '127.0.0.1', port: 1502, unitId: 1, register: 40001, registerType: 'holding', dataType: 'float32', scale: 1, byteOrder: 'big' }, unit: 'MPa', min: 0, max: 5, intervalMs: 2000, lineId: line.id },
|
|
100
|
+
token,
|
|
101
|
+
})).data?.node
|
|
102
|
+
const product = (await api('POST', '/api/workshop/dcw/products', { body: { name: `mh-prod-${TAG}`, lineId: line.id }, token })).data?.product
|
|
103
|
+
const recipe = (await api('POST', '/api/workshop/dcw/recipes', {
|
|
104
|
+
body: { name: `mh-recipe-${TAG}`, productId: product.id, params: [{ nodeId: nodeA.id, value: 0.8, min: 0.6, max: 1.1 }] },
|
|
105
|
+
token,
|
|
106
|
+
})).data?.recipe
|
|
107
|
+
ok(Boolean(line?.id && nodeA?.id && daq?.id && recipe?.id), `夹具:产线/节点/数采/配方(${recipe?.id} v${recipe?.version})`)
|
|
108
|
+
|
|
109
|
+
// 产线管理生命周期:开跑
|
|
110
|
+
const start = await api('POST', `/api/workshop/dcw/lines/${line.id}/start`, { body: { recipeId: recipe.id }, token })
|
|
111
|
+
ok(start.status === 200, '[产线管理] 开跑(绑定批次)')
|
|
112
|
+
await sleep(4000)
|
|
113
|
+
const daqSnap = (await api('GET', '/api/workshop/daq', { token })).data
|
|
114
|
+
const daqLive = daqSnap.nodes.find(n => n.id === daq.id)
|
|
115
|
+
ok(daqLive?.value != null, `[产线管理] 数采真实值流入(${daqLive?.name}=${daqLive?.value}${daqLive?.unit ?? ''})`)
|
|
116
|
+
|
|
117
|
+
// ════ 四引擎并行组队 + 绑定 ════
|
|
118
|
+
const members = {}
|
|
119
|
+
for (const e of ENGINES) {
|
|
120
|
+
const ch = (await api('POST', '/api/workshop/channels', {
|
|
121
|
+
body: { name: `mh-${e.key}-${TAG}`, leadAgent: { name: `lead-${TAG}`, harness: 'mock', config: { delayMs: 60 } } },
|
|
122
|
+
token,
|
|
123
|
+
})).data
|
|
124
|
+
const tpl = await api('POST', '/api/workshop/agents', {
|
|
125
|
+
body: { name: `mh-${e.key}-${TAG}`, harness: e.harness, config: { ...e.cfg, systemPromptPrefix: '你是产线操作员:严格按任务步骤执行,只做要求的事,完成后立即调用 complete_task。' } },
|
|
126
|
+
token,
|
|
127
|
+
})
|
|
128
|
+
const join = await api('POST', `/api/workshop/channels/${ch.channelId}/agents`, { body: { agentId: tpl.data.id, role: 'worker' }, token })
|
|
129
|
+
const instId = join.data?.id
|
|
130
|
+
await api('POST', '/api/workshop/agent-tools/bindings', { body: { agentId: instId, nodeId: nodeA.id, kind: 'dcw', mode: 'auto' }, token })
|
|
131
|
+
await api('POST', '/api/workshop/agent-tools/bindings', { body: { agentId: instId, nodeId: daq.id, kind: 'daq', mode: 'auto' }, token })
|
|
132
|
+
members[e.key] = { channelId: ch.channelId, instId, tplId: tpl.data.id }
|
|
133
|
+
ok(Boolean(instId), `[${e.harness}] worker 建号+绑定`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ════ 分场景任务(并行下发)════
|
|
137
|
+
const tasks = {}
|
|
138
|
+
const scenario = (key, body) => api('POST', `/api/workshop/channels/${members[key].channelId}/tasks`, { body: { ...body, assigneeId: members[key].instId }, token })
|
|
139
|
+
|
|
140
|
+
tasks.omp = (await scenario('omp', {
|
|
141
|
+
title: `mh-loop-omp-${TAG}`,
|
|
142
|
+
parts: [{ text: `闭环调优(不要用其它节点):
|
|
143
|
+
1. dcw_control(node_id="${nodeA.id}", value=0.82, hypothesis="提压观察");
|
|
144
|
+
2. 等待 3 秒后 daq_query(node_id="${daq.id}", last_minutes=5);
|
|
145
|
+
3. 依据采样证据 dcw_judge 对该优化记录落 keep;
|
|
146
|
+
4. 交付原样包含一行:OMP-CLOSEDLOOP-OK,并附设定值与采样值。
|
|
147
|
+
完成后 complete_task。` }],
|
|
148
|
+
})).data?.id
|
|
149
|
+
tasks.codex = (await scenario('codex', {
|
|
150
|
+
title: `mh-write-codex-${TAG}`,
|
|
151
|
+
parts: [{ text: `数据控制操作:
|
|
152
|
+
1. dcw_control(node_id="${nodeA.id}", value=0.78) 下发压力设定;
|
|
153
|
+
2. dcw_journal(node_id="${nodeA.id}") 查看参数账本;
|
|
154
|
+
3. 交付原样包含一行:CODEX-WRITE-OK,附设定值。
|
|
155
|
+
完成后 complete_task。` }],
|
|
156
|
+
})).data?.id
|
|
157
|
+
tasks.dsh = (await scenario('dsh', {
|
|
158
|
+
title: `mh-daq-dsh-${TAG}`,
|
|
159
|
+
parts: [{ text: `数据采集操作(不要下发任何控制):
|
|
160
|
+
1. line_context 查看你负责的产线/产品/配方;
|
|
161
|
+
2. daq_query(node_id="${daq.id}", last_minutes=10) 获取真实采样;
|
|
162
|
+
3. 交付原样包含一行:DSH-DAQ-OK,并引用一个采样数值。
|
|
163
|
+
完成后 complete_task。` }],
|
|
164
|
+
})).data?.id
|
|
165
|
+
const verBefore = (await api('GET', `/api/workshop/dcw/recipes/${recipe.id}/versions`, { token })).data?.versions ?? []
|
|
166
|
+
const vBefore = verBefore[verBefore.length - 1]?.version ?? 1
|
|
167
|
+
tasks.opencode = (await scenario('opencode', {
|
|
168
|
+
title: `mh-recipe-oc-${TAG}`,
|
|
169
|
+
parts: [{ text: `Recipe 写入回退操作:
|
|
170
|
+
1. recipe_update(recipe_id="${recipe.id}", params=[{node_id:"${nodeA.id}", value:0.88}], reason="数采验证窗口稳定,固化最佳值");
|
|
171
|
+
2. recipe_rollback(recipe_id="${recipe.id}", version=${vBefore}, reason="E2E 回退演练");
|
|
172
|
+
3. recipe_versions 复核;
|
|
173
|
+
4. 交付原样包含一行:OC-RECIPE-OK,并附保存与回退的版本号。
|
|
174
|
+
完成后 complete_task。` }],
|
|
175
|
+
})).data?.id
|
|
176
|
+
ok(Object.values(tasks).every(Boolean), '四场景任务并行下发')
|
|
177
|
+
|
|
178
|
+
// ════ 等待全部终态(并行;单次窥探防超时堆积)════
|
|
179
|
+
const states = {}
|
|
180
|
+
const peekTask = async (id) => {
|
|
181
|
+
for (let i = 0; i < 3; i++) {
|
|
182
|
+
try {
|
|
183
|
+
return (await api('GET', `/api/workshop/tasks/${id}`, { token })).data?.state ?? ''
|
|
184
|
+
}
|
|
185
|
+
catch { await sleep(3000) }
|
|
186
|
+
}
|
|
187
|
+
return ''
|
|
188
|
+
}
|
|
189
|
+
const deadline = Date.now() + 16 * 60_000
|
|
190
|
+
while (Date.now() < deadline && Object.keys(states).length < ENGINES.length) {
|
|
191
|
+
for (const e of ENGINES) {
|
|
192
|
+
if (states[e.key]) continue
|
|
193
|
+
const st = await peekTask(tasks[e.key])
|
|
194
|
+
if (['COMPLETED', 'FAILED', 'CANCELED'].includes(st)) states[e.key] = st
|
|
195
|
+
}
|
|
196
|
+
if (Object.keys(states).length < ENGINES.length) await sleep(6000)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ════ 逐引擎断言 ════
|
|
200
|
+
const blobCache = {}
|
|
201
|
+
const blobOf = async (key) => {
|
|
202
|
+
if (!blobCache[key]) blobCache[key] = await gatherTaskBlob(token, members[key].channelId, tasks[key])
|
|
203
|
+
return blobCache[key]
|
|
204
|
+
}
|
|
205
|
+
for (const e of ENGINES) {
|
|
206
|
+
const st = states[e.key]
|
|
207
|
+
ok(st === 'COMPLETED', `[${e.harness}] 任务 COMPLETED(state=${st || 'RUNNING'})`)
|
|
208
|
+
blobCache[e.key] = await gatherTaskBlob(token, members[e.key].channelId, tasks[e.key])
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const bOmp = await blobOf('omp')
|
|
212
|
+
ok(bOmp.includes('OMP-CLOSEDLOOP-OK'), '[omp] 交付含 OMP-CLOSEDLOOP-OK(真实下发+采样+判定)')
|
|
213
|
+
|
|
214
|
+
const wRead = await api('POST', `/api/workshop/dcw/${nodeA.id}/read`, { body: {}, token })
|
|
215
|
+
const lastVal = wRead.data?.read?.value ?? wRead.data?.read?.eng
|
|
216
|
+
const bCodex = await blobOf('codex')
|
|
217
|
+
ok(bCodex.includes('CODEX-WRITE-OK'), '[codex] 交付含 CODEX-WRITE-OK(真实 Modbus 写入)')
|
|
218
|
+
|
|
219
|
+
const bDsh = await blobOf('dsh')
|
|
220
|
+
ok(bDsh.includes('DSH-DAQ-OK'), '[dsh] 交付含 DSH-DAQ-OK(真实数采)')
|
|
221
|
+
|
|
222
|
+
const versAfter = (await api('GET', `/api/workshop/dcw/recipes/${recipe.id}/versions`, { token })).data?.versions ?? []
|
|
223
|
+
const vAfter = versAfter[versAfter.length - 1]?.version ?? 1
|
|
224
|
+
const agentEntries = versAfter.filter(v => v.by === 'agent')
|
|
225
|
+
ok(vAfter >= vBefore + 2, `[opencode] Recipe 版本净增≥2(${vBefore}→${vAfter}:保存+回退)`)
|
|
226
|
+
ok(agentEntries.some(v => (v.actorName ?? '').includes(`mh-opencode-${TAG}`)), `[opencode] 版本归因含 opencode worker 名(${agentEntries.map(v => v.actorName).join(';') || '∅'})`)
|
|
227
|
+
const bOc = await blobOf('opencode')
|
|
228
|
+
ok(bOc.includes('OC-RECIPE-OK'), '[opencode] 交付含 OC-RECIPE-OK(Recipe 写入+回退)')
|
|
229
|
+
void lastVal
|
|
230
|
+
|
|
231
|
+
// ════ 产线管理:停线生命周期 ════
|
|
232
|
+
const stop = await api('POST', `/api/workshop/dcw/lines/${line.id}/stop`, { body: {}, token })
|
|
233
|
+
ok(stop.status === 200, '[产线管理] 停线')
|
|
234
|
+
await sleep(2500)
|
|
235
|
+
const daqAfterStop = (await api('GET', '/api/workshop/daq', { token })).data.nodes.find(n => n.id === daq.id)
|
|
236
|
+
ok(daqAfterStop?.state === 'offline' || daqAfterStop?.state === 'idle', `[产线管理] 停线后节点状态=${daqAfterStop?.state}(采集门控生效)`)
|
|
237
|
+
|
|
238
|
+
// ════ 清理 ════
|
|
239
|
+
for (const m of Object.values(members)) await api('DELETE', `/api/workshop/channels/${m.channelId}?purge=1`, { token }).catch(() => {})
|
|
240
|
+
for (const m of Object.values(members)) await api('DELETE', `/api/workshop/agents/${m.tplId}`, { token }).catch(() => {})
|
|
241
|
+
await api('DELETE', `/api/workshop/dcw/recipes/${recipe.id}`, { token }).catch(() => {})
|
|
242
|
+
await api('DELETE', `/api/workshop/dcw/products/${product.id}`, { token }).catch(() => {})
|
|
243
|
+
await api('DELETE', `/api/workshop/dcw/${nodeA.id}`, { token }).catch(() => {})
|
|
244
|
+
await api('DELETE', `/api/workshop/daq/${daq.id}`, { token }).catch(() => {})
|
|
245
|
+
await api('DELETE', `/api/workshop/dcw/lines/${line.id}`, { token }).catch(() => {})
|
|
246
|
+
|
|
247
|
+
console.log(`\n━━━ MultiHarness Live E2E: ${pass} passed / ${fail} failed ━━━`)
|
|
248
|
+
process.exit(fail === 0 ? 0 : 1)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
main().catch((err) => {
|
|
252
|
+
console.error('E2E 异常:', err)
|
|
253
|
+
process.exit(1)
|
|
254
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const net = require('net')
|
|
2
|
+
|
|
3
|
+
const s = net.connect({ host: '127.0.0.1', port: 5432, timeout: 4000 })
|
|
4
|
+
s.on('connect', () => {
|
|
5
|
+
console.log('TCP connected')
|
|
6
|
+
// 发送 PG StartupMessage(protocol 3.0, user postgres, db awshop)
|
|
7
|
+
const params = ['user', 'postgres', 'database', 'awshop']
|
|
8
|
+
const body = Buffer.from([0, 3, ...Buffer.from(params.join('\0') + '\0', 'utf8'), 0])
|
|
9
|
+
const head = Buffer.alloc(8)
|
|
10
|
+
head.writeInt32BE(8 + body.length, 0)
|
|
11
|
+
s.write(head)
|
|
12
|
+
})
|
|
13
|
+
s.on('data', (d) => {
|
|
14
|
+
console.log('resp type:', String.fromCharCode(d[0]), 'len:', d.readInt32BE(1))
|
|
15
|
+
s.destroy()
|
|
16
|
+
process.exit(0)
|
|
17
|
+
})
|
|
18
|
+
s.on('timeout', () => {
|
|
19
|
+
console.log('TIMEOUT')
|
|
20
|
+
s.destroy()
|
|
21
|
+
process.exit(1)
|
|
22
|
+
})
|
|
23
|
+
s.on('error', (e) => {
|
|
24
|
+
console.log('ERR:', e.message)
|
|
25
|
+
process.exit(1)
|
|
26
|
+
})
|
|
27
|
+
s.on('close', had => console.log('closed, hadError=', had))
|