agentworkshop 0.7.18 → 0.7.19

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentworkshop-prod",
3
- "version": "0.7.18",
3
+ "version": "0.7.19",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "dependencies": {
package/package.json CHANGED
@@ -1,148 +1,148 @@
1
- {
2
- "name": "agentworkshop",
3
- "version": "0.7.18",
4
- "type": "module",
5
- "description": "AgentWorkShop 软件开发系统 - 基于 Nuxt 4 的配置驱动运行范式",
6
- "license": "PolyForm-Noncommercial-1.0.0",
7
- "exports": {
8
- ".": {
9
- "types": "./sdk/index.d.mts",
10
- "default": "./sdk/index.mjs"
11
- },
12
- "./sdk": {
13
- "types": "./sdk/index.d.mts",
14
- "default": "./sdk/index.mjs"
15
- },
16
- "./sdk/client": {
17
- "types": "./sdk/client.d.mts",
18
- "default": "./sdk/client.mjs"
19
- },
20
- "./package.json": "./package.json"
21
- },
22
- "bin": {
23
- "aw": "bin/aw.mjs",
24
- "agentworkshop": "bin/aw.mjs"
25
- },
26
- "files": [
27
- ".output",
28
- "bin",
29
- "cli",
30
- "sdk",
31
- "app",
32
- "server",
33
- "shared",
34
- "i18n",
35
- "public",
36
- "scripts",
37
- ".AgentWorkShop/prompts",
38
- "config.yml",
39
- "nuxt.config.ts",
40
- "tsconfig.json",
41
- "uno.config.ts",
42
- "docker-compose.yml",
43
- ".env.example",
44
- "README-zh.md",
45
- "docs/cli.md",
46
- "docs/plugins.md",
47
- "docs/sdk.md",
48
- "docs/tui.md",
49
- "tui"
50
- ],
51
- "packageManager": "pnpm@11.9.0",
52
- "engines": {
53
- "node": ">=23.4.0"
54
- },
55
- "scripts": {
56
- "dev": "node bin/aw.mjs dev",
57
- "build": "node bin/aw.mjs build",
58
- "start": "node bin/aw.mjs start",
59
- "generate": "nuxt generate",
60
- "preview": "node bin/aw.mjs start",
61
- "cli": "node bin/aw.mjs",
62
- "tui": "node tui/aw-tui.mjs",
63
- "postinstall": "node scripts/home-bootstrap.mjs",
64
- "prepublishOnly": "node bin/aw.mjs build",
65
- "lint": "eslint .",
66
- "lint:fix": "eslint . --fix",
67
- "typecheck": "nuxt typecheck",
68
- "prepare": "husky",
69
- "game:test": "tsx scripts/test-agent-brain.ts && tsx scripts/test-game-session.ts",
70
- "test:api-live": "node scripts/api-live-e2e.mjs",
71
- "game:verify": "node scripts/verify-game.mjs",
72
- "game:build-assets": "node scripts/build-player-sheet.mjs",
73
- "game:build-wuwa": "node scripts/build-wuwa-assets.mjs",
74
- "game:build-models": "node scripts/build-character-models.mjs",
75
- "game:build-world": "node scripts/build-world-map.mjs",
76
- "game:build-model3d": "node scripts/build-model-3d.mjs"
77
- },
78
- "lint-staged": {
79
- "*.{js,mjs,cjs,ts,vue}": "eslint --fix"
80
- },
81
- "dependencies": {
82
- "@ant-design-vue/nuxt": "^1.4.6",
83
- "@ant-design/icons-vue": "^7.0.1",
84
- "@earendil-works/pi-tui": "^0.84.4",
85
- "@fontsource-variable/fraunces": "^5.3.0",
86
- "@fontsource-variable/geist": "^5.3.0",
87
- "@fontsource-variable/geist-mono": "^5.3.0",
88
- "@fontsource-variable/inter": "^5.3.0",
89
- "@fontsource/eb-garamond": "^5.3.0",
90
- "@fontsource/ibm-plex-mono": "^5.3.0",
91
- "@iconify-json/tabler": "^1.2.0",
92
- "@modelcontextprotocol/sdk": "^1.30.0",
93
- "@nuxtjs/i18n": "^9.0.0",
94
- "@pinia/nuxt": "^0.11.0",
95
- "@unocss/nuxt": "^66.0.0",
96
- "@unocss/preset-attributify": "^66.0.0",
97
- "@unocss/preset-icons": "^66.7.5",
98
- "@unocss/preset-typography": "^66.0.0",
99
- "@vueuse/core": "^13.0.0",
100
- "@vueuse/nuxt": "^13.0.0",
101
- "@xterm/addon-fit": "^0.11.0",
102
- "@xterm/xterm": "^6.0.0",
103
- "ant-design-vue": "^4.2.6",
104
- "axios": "^1.7.9",
105
- "echarts": "^5.6.0",
106
- "h3": "2.0.1-rc.26",
107
- "js-yaml": "^4.1.0",
108
- "minio": "^8.0.7",
109
- "modbus-serial": "^8.0.25",
110
- "mqtt": "^5.15.2",
111
- "node-opcua": "^2.178.0",
112
- "nuxt": "^4.0.0",
113
- "nuxt-auth-utils": "^0.5.0",
114
- "pg": "^8.23.0",
115
- "phaser": "^4.0.0",
116
- "pinia": "^3.0.0",
117
- "pinia-plugin-persistedstate": "^4.2.0",
118
- "sqlite-vec": "^0.1.9",
119
- "three": "^0.185.1",
120
- "unocss": "^66.0.0",
121
- "unplugin-icons": "^22.0.0",
122
- "vue": "^3.5.13",
123
- "vue-echarts": "^7.0.3",
124
- "vue-router": "^4.5.0",
125
- "zod": "^3.24.1"
126
- },
127
- "devDependencies": {
128
- "@commitlint/cli": "^19.6.1",
129
- "@commitlint/config-conventional": "^19.6.0",
130
- "@nuxt/eslint": "^1.0.0",
131
- "@types/js-yaml": "^4.0.9",
132
- "@types/node": "^26.2.0",
133
- "@types/three": "^0.185.4",
134
- "eslint": "^9.17.0",
135
- "husky": "^9.1.7",
136
- "lint-staged": "^15.3.0",
137
- "pngjs": "^7.0.0",
138
- "puppeteer-core": "^25.5.0",
139
- "tsx": "^4.23.12",
140
- "typescript": "^5.7.3",
141
- "vue-tsc": "^3.3.10"
142
- },
143
- "pnpm": {
144
- "ignoredBuiltDependencies": [
145
- "@serialport/bindings-cpp"
146
- ]
147
- }
148
- }
1
+ {
2
+ "name": "agentworkshop",
3
+ "version": "0.7.19",
4
+ "type": "module",
5
+ "description": "AgentWorkShop 软件开发系统 - 基于 Nuxt 4 的配置驱动运行范式",
6
+ "license": "PolyForm-Noncommercial-1.0.0",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./sdk/index.d.mts",
10
+ "default": "./sdk/index.mjs"
11
+ },
12
+ "./sdk": {
13
+ "types": "./sdk/index.d.mts",
14
+ "default": "./sdk/index.mjs"
15
+ },
16
+ "./sdk/client": {
17
+ "types": "./sdk/client.d.mts",
18
+ "default": "./sdk/client.mjs"
19
+ },
20
+ "./package.json": "./package.json"
21
+ },
22
+ "bin": {
23
+ "aw": "bin/aw.mjs",
24
+ "agentworkshop": "bin/aw.mjs"
25
+ },
26
+ "files": [
27
+ ".output",
28
+ "bin",
29
+ "cli",
30
+ "sdk",
31
+ "app",
32
+ "server",
33
+ "shared",
34
+ "i18n",
35
+ "public",
36
+ "scripts",
37
+ ".AgentWorkShop/prompts",
38
+ "config.yml",
39
+ "nuxt.config.ts",
40
+ "tsconfig.json",
41
+ "uno.config.ts",
42
+ "docker-compose.yml",
43
+ ".env.example",
44
+ "README-zh.md",
45
+ "docs/cli.md",
46
+ "docs/plugins.md",
47
+ "docs/sdk.md",
48
+ "docs/tui.md",
49
+ "tui"
50
+ ],
51
+ "packageManager": "pnpm@11.9.0",
52
+ "engines": {
53
+ "node": ">=23.4.0"
54
+ },
55
+ "scripts": {
56
+ "dev": "node bin/aw.mjs dev",
57
+ "build": "node bin/aw.mjs build",
58
+ "start": "node bin/aw.mjs start",
59
+ "generate": "nuxt generate",
60
+ "preview": "node bin/aw.mjs start",
61
+ "cli": "node bin/aw.mjs",
62
+ "tui": "node tui/aw-tui.mjs",
63
+ "postinstall": "node scripts/home-bootstrap.mjs",
64
+ "prepublishOnly": "node bin/aw.mjs build",
65
+ "lint": "eslint .",
66
+ "lint:fix": "eslint . --fix",
67
+ "typecheck": "nuxt typecheck",
68
+ "prepare": "husky",
69
+ "game:test": "tsx scripts/test-agent-brain.ts && tsx scripts/test-game-session.ts",
70
+ "test:api-live": "node scripts/api-live-e2e.mjs",
71
+ "game:verify": "node scripts/verify-game.mjs",
72
+ "game:build-assets": "node scripts/build-player-sheet.mjs",
73
+ "game:build-wuwa": "node scripts/build-wuwa-assets.mjs",
74
+ "game:build-models": "node scripts/build-character-models.mjs",
75
+ "game:build-world": "node scripts/build-world-map.mjs",
76
+ "game:build-model3d": "node scripts/build-model-3d.mjs"
77
+ },
78
+ "lint-staged": {
79
+ "*.{js,mjs,cjs,ts,vue}": "eslint --fix"
80
+ },
81
+ "dependencies": {
82
+ "@ant-design-vue/nuxt": "^1.4.6",
83
+ "@ant-design/icons-vue": "^7.0.1",
84
+ "@earendil-works/pi-tui": "^0.84.4",
85
+ "@fontsource-variable/fraunces": "^5.3.0",
86
+ "@fontsource-variable/geist": "^5.3.0",
87
+ "@fontsource-variable/geist-mono": "^5.3.0",
88
+ "@fontsource-variable/inter": "^5.3.0",
89
+ "@fontsource/eb-garamond": "^5.3.0",
90
+ "@fontsource/ibm-plex-mono": "^5.3.0",
91
+ "@iconify-json/tabler": "^1.2.0",
92
+ "@modelcontextprotocol/sdk": "^1.30.0",
93
+ "@nuxtjs/i18n": "^9.0.0",
94
+ "@pinia/nuxt": "^0.11.0",
95
+ "@unocss/nuxt": "^66.0.0",
96
+ "@unocss/preset-attributify": "^66.0.0",
97
+ "@unocss/preset-icons": "^66.7.5",
98
+ "@unocss/preset-typography": "^66.0.0",
99
+ "@vueuse/core": "^13.0.0",
100
+ "@vueuse/nuxt": "^13.0.0",
101
+ "@xterm/addon-fit": "^0.11.0",
102
+ "@xterm/xterm": "^6.0.0",
103
+ "ant-design-vue": "^4.2.6",
104
+ "axios": "^1.7.9",
105
+ "echarts": "^5.6.0",
106
+ "h3": "2.0.1-rc.26",
107
+ "js-yaml": "^4.1.0",
108
+ "minio": "^8.0.7",
109
+ "modbus-serial": "^8.0.25",
110
+ "mqtt": "^5.15.2",
111
+ "node-opcua": "^2.178.0",
112
+ "nuxt": "^4.0.0",
113
+ "nuxt-auth-utils": "^0.5.0",
114
+ "pg": "^8.23.0",
115
+ "phaser": "^4.0.0",
116
+ "pinia": "^3.0.0",
117
+ "pinia-plugin-persistedstate": "^4.2.0",
118
+ "sqlite-vec": "^0.1.9",
119
+ "three": "^0.185.1",
120
+ "unocss": "^66.0.0",
121
+ "unplugin-icons": "^22.0.0",
122
+ "vue": "^3.5.13",
123
+ "vue-echarts": "^7.0.3",
124
+ "vue-router": "^4.5.0",
125
+ "zod": "^3.24.1"
126
+ },
127
+ "devDependencies": {
128
+ "@commitlint/cli": "^19.6.1",
129
+ "@commitlint/config-conventional": "^19.6.0",
130
+ "@nuxt/eslint": "^1.0.0",
131
+ "@types/js-yaml": "^4.0.9",
132
+ "@types/node": "^26.2.0",
133
+ "@types/three": "^0.185.4",
134
+ "eslint": "^9.17.0",
135
+ "husky": "^9.1.7",
136
+ "lint-staged": "^15.3.0",
137
+ "pngjs": "^7.0.0",
138
+ "puppeteer-core": "^25.5.0",
139
+ "tsx": "^4.23.12",
140
+ "typescript": "^5.7.3",
141
+ "vue-tsc": "^3.3.10"
142
+ },
143
+ "pnpm": {
144
+ "ignoredBuiltDependencies": [
145
+ "@serialport/bindings-cpp"
146
+ ]
147
+ }
148
+ }
@@ -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,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))