agentworkshop 0.7.21 → 0.7.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/BJbhwS5o.js +1 -0
  3. package/.output/public/_nuxt/Bo8t5fkJ.js +1 -0
  4. package/.output/public/_nuxt/{DBXhDLgl.js → BuiGDfMB.js} +1 -1
  5. package/.output/public/_nuxt/{QT54MQV9.js → C53drffV.js} +1 -1
  6. package/.output/public/_nuxt/{BA3XzwiD.js → CEHmSufQ.js} +2 -2
  7. package/.output/public/_nuxt/Dc32T41C.js +1 -0
  8. package/.output/public/_nuxt/DtzZqC0z.js +1 -0
  9. package/.output/public/_nuxt/{BOJaHO5V.js → Ko9mMsJj.js} +3 -3
  10. package/.output/public/_nuxt/{TownView.BHLKAjEu.css → TownView.zu2rU5nS.css} +1 -1
  11. package/.output/public/_nuxt/_id_.C-YXjn8Q.css +1 -0
  12. package/.output/public/_nuxt/_id_.MbzdFygI.css +1 -0
  13. package/.output/public/_nuxt/_wsId_.BqWBlY7X.css +1 -0
  14. package/.output/public/_nuxt/builds/latest.json +1 -1
  15. package/.output/public/_nuxt/builds/meta/1adc96e0-8bbd-4a79-ac7e-b6952556fef8.json +1 -0
  16. package/.output/public/_nuxt/daq.3OKj58HH.css +1 -0
  17. package/.output/public/_nuxt/dbi5bHkY.js +2 -0
  18. package/.output/public/_nuxt/dcw.rXH2fc2Z.css +1 -0
  19. package/.output/public/_nuxt/entry.OR_psZHp.css +1 -0
  20. package/.output/public/_nuxt/{DsOkkMX3.js → jyQyulh-.js} +1 -1
  21. package/.output/public/_nuxt/logs.DMOM-iHe.css +1 -0
  22. package/.output/public/_nuxt/{B-E8fXAO.js → n3vMFfrv.js} +1 -1
  23. package/.output/server/chunks/_/nitro.mjs +842 -717
  24. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  25. package/.output/server/chunks/build/{TownView-ql2eHZOg.mjs → TownView-DxeQe1Bl.mjs} +144 -143
  26. package/.output/server/chunks/build/{TownView-ql2eHZOg.mjs.map → TownView-DxeQe1Bl.mjs.map} +1 -1
  27. package/.output/server/chunks/build/{_id_-CwR5oLu9.mjs → _id_-DQDqj-cZ.mjs} +33 -35
  28. package/.output/server/chunks/build/_id_-DQDqj-cZ.mjs.map +1 -0
  29. package/.output/server/chunks/build/{_id_-Bs-euJPu.mjs → _id_-DY6v6sqB.mjs} +138 -137
  30. package/.output/server/chunks/build/_id_-DY6v6sqB.mjs.map +1 -0
  31. package/.output/server/chunks/build/{_wsId_-B3aB9bIp.mjs → _wsId_-C2O5zZsI.mjs} +6 -6
  32. package/.output/server/chunks/build/{_wsId_-B3aB9bIp.mjs.map → _wsId_-C2O5zZsI.mjs.map} +1 -1
  33. package/.output/server/chunks/build/{daq-epWL_sC7.mjs → daq-B-3xCp_2.mjs} +146 -118
  34. package/.output/server/chunks/build/daq-B-3xCp_2.mjs.map +1 -0
  35. package/.output/server/chunks/build/{dcw-CBPaFj2f.mjs → dcw-BL2dhuz7.mjs} +43 -43
  36. package/.output/server/chunks/build/{dcw-CBPaFj2f.mjs.map → dcw-BL2dhuz7.mjs.map} +1 -1
  37. package/.output/server/chunks/build/{logs-Bz1p6x_5.mjs → logs-Dto_umFY.mjs} +44 -38
  38. package/.output/server/chunks/build/logs-Dto_umFY.mjs.map +1 -0
  39. package/.output/server/chunks/build/{pages-Dq7_H4FK.mjs → pages-D9tOoJsf.mjs} +2 -2
  40. package/.output/server/chunks/build/{pages-Dq7_H4FK.mjs.map → pages-D9tOoJsf.mjs.map} +1 -1
  41. package/.output/server/chunks/build/{town-CstiKxf_.mjs → town-Dsh0jlwZ.mjs} +3 -3
  42. package/.output/server/chunks/build/{town-CstiKxf_.mjs.map → town-Dsh0jlwZ.mjs.map} +1 -1
  43. package/.output/server/chunks/build/{useDaqStream-BOOZ-p-l.mjs → useDaqStream-DkRL3tEW.mjs} +9 -1
  44. package/.output/server/chunks/build/{useDaqStream-BOOZ-p-l.mjs.map → useDaqStream-DkRL3tEW.mjs.map} +1 -1
  45. package/.output/server/chunks/routes/api/workshop/daq/_id/bind.post.mjs +1 -1
  46. package/.output/server/chunks/routes/api/workshop/daq/_id/bind.post.mjs.map +1 -1
  47. package/.output/server/chunks/routes/api/workshop/daq/_id/bindings.put.mjs +34 -0
  48. package/.output/server/chunks/routes/api/workshop/daq/_id/bindings.put.mjs.map +1 -0
  49. package/.output/server/chunks/routes/api/workshop/dcw/_id/bind.post.mjs +1 -1
  50. package/.output/server/chunks/routes/api/workshop/dcw/_id/bind.post.mjs.map +1 -1
  51. package/.output/server/chunks/routes/api/workshop/dcw/_id/bindings.put.mjs +33 -0
  52. package/.output/server/chunks/routes/api/workshop/dcw/_id/bindings.put.mjs.map +1 -0
  53. package/.output/server/chunks/routes/renderer.mjs +1 -1
  54. package/.output/server/chunks/virtual/entry.mjs +16 -8
  55. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  56. package/.output/server/package.json +1 -1
  57. package/app/assets/css/main.css +306 -161
  58. package/app/components/workshop/blocks/ClusterArtifact.vue +0 -16
  59. package/app/components/workshop/town/TownView.vue +7082 -7076
  60. package/app/composables/workshop/useDaqStream.ts +10 -0
  61. package/app/pages/daq/[id].vue +919 -937
  62. package/app/pages/daq/index.vue +67 -43
  63. package/app/pages/dcw/[id].vue +6 -5
  64. package/app/pages/dcw/index.vue +1 -30
  65. package/app/pages/logs/index.vue +99 -87
  66. package/i18n/locales/en.ts +3 -0
  67. package/i18n/locales/zh-CN.ts +5 -0
  68. package/package.json +148 -148
  69. package/scripts/_dbg-device-bind-e2e.mjs +129 -0
  70. package/scripts/_dbg-logs-ui-e2e.py +125 -0
  71. package/server/api/workshop/daq/[id]/bind.post.ts +5 -2
  72. package/server/api/workshop/daq/[id]/bindings.put.ts +22 -0
  73. package/server/api/workshop/dcw/[id]/bind.post.ts +4 -2
  74. package/server/api/workshop/dcw/[id]/bindings.put.ts +21 -0
  75. package/server/data/daqs.json +405 -100
  76. package/server/data/dcw-lines.json +7 -0
  77. package/server/data/dcw-rollback.json +21 -10
  78. package/server/data/dcw-runs.json +50 -54
  79. package/server/data/dcw-writes.json +24 -0
  80. package/server/data/dcws.json +505 -216
  81. package/server/data/line-runs.json +18 -18
  82. package/server/services/workshop/agents/industrial-context.ts +231 -222
  83. package/server/services/workshop/daq/daq-controller.ts +76 -23
  84. package/server/services/workshop/daq/daq-node.ts +11 -1
  85. package/server/services/workshop/dcw/dcw-controller.ts +50 -2
  86. package/server/services/workshop/dcw/dcw-node.ts +10 -1
  87. package/shared/daq-protocol.ts +2 -0
  88. package/shared/dcw-protocol.ts +2 -0
  89. package/.output/public/_nuxt/B_C3GLsD.js +0 -1
  90. package/.output/public/_nuxt/Ba7YofCb.js +0 -2
  91. package/.output/public/_nuxt/C_2bTK0C.js +0 -1
  92. package/.output/public/_nuxt/CgT5wMfz.js +0 -1
  93. package/.output/public/_nuxt/D4CLp1hc.js +0 -1
  94. package/.output/public/_nuxt/_id_.DIxEKj88.css +0 -1
  95. package/.output/public/_nuxt/_id_.IbmpkhH4.css +0 -1
  96. package/.output/public/_nuxt/_wsId_.BFyCuUwS.css +0 -1
  97. package/.output/public/_nuxt/builds/meta/5d344836-03a5-4f47-a0a7-fd30192f5a8f.json +0 -1
  98. package/.output/public/_nuxt/daq.6IrhVVfy.css +0 -1
  99. package/.output/public/_nuxt/dcw.rpqpOYQ8.css +0 -1
  100. package/.output/public/_nuxt/entry.DRUzDwmX.css +0 -1
  101. package/.output/public/_nuxt/logs.BarkrvUp.css +0 -1
  102. package/.output/server/chunks/build/_id_-Bs-euJPu.mjs.map +0 -1
  103. package/.output/server/chunks/build/_id_-CwR5oLu9.mjs.map +0 -1
  104. package/.output/server/chunks/build/daq-epWL_sC7.mjs.map +0 -1
  105. package/.output/server/chunks/build/logs-Bz1p6x_5.mjs.map +0 -1
package/package.json CHANGED
@@ -1,148 +1,148 @@
1
- {
2
- "name": "agentworkshop",
3
- "version": "0.7.21",
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.23",
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,129 @@
1
+ /**
2
+ * 节点 ↔ 设备 多对多绑定 E2E(隔离实例 :3021)
3
+ *
4
+ * 验证:
5
+ * 1) legacy 单绑 POST bind {deviceId} → deviceIds=[d1](迁移语义)
6
+ * 2) PUT bindings [d1,d2] → 两台设备;重复设定不产生重复对
7
+ * 3) 数采回写:节点采样值同时出现在 d1/d2 的 telemetry(多对多回写)
8
+ * 4) 摘除 d2 → 仅 d1 继续回写;d2 的 telemetry 冻结在摘除时刻
9
+ * 5) 级联:删除 d1 → 节点 deviceIds 自动移除 d1(保留 d2)
10
+ * 6) dcw 同模型:PUT bindings 双设备 + legacy 兼容
11
+ * 7) 负向:绑定不存在的设备 404
12
+ * 用法:NO_PROXY='127.0.0.1,localhost' node scripts/_dbg-device-bind-e2e.mjs [base]
13
+ */
14
+ const BASE = process.argv[2] ?? 'http://127.0.0.1:3021'
15
+ const TAG = `db${Math.random().toString(36).slice(2, 6)}`
16
+ let pass = 0
17
+ let fail = 0
18
+ const ok = (cond, name, extra = '') => {
19
+ if (cond) { pass++; console.log(` ✓ ${name}`) }
20
+ else { fail++; console.log(` ✗ ${name} ${extra}`) }
21
+ }
22
+ const sleep = ms => new Promise(r => setTimeout(r, ms))
23
+ const api = async (method, path, { body, token } = {}) => {
24
+ const res = await fetch(`${BASE}${path}`, {
25
+ method,
26
+ headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
27
+ body: body === undefined ? undefined : JSON.stringify(body),
28
+ signal: AbortSignal.timeout(30_000),
29
+ })
30
+ return { status: res.status, ...(await res.json().catch(() => ({}))) }
31
+ }
32
+
33
+ async function main() {
34
+ console.log(`━━━ 节点↔设备多对多绑定 E2E @ ${BASE} (tag=${TAG}) ━━━`)
35
+ const login = await api('POST', '/api/users/login', { body: { email: 'admin', password: 'admin123' } })
36
+ const token = login.data?.token
37
+ ok(Boolean(token), 'admin 登录')
38
+
39
+ // ── 夹具:2 设备 + 1 数采节点(mock)+ 1 数控节点(mock)──
40
+ const d1 = (await api('POST', '/api/workshop/device-twins', { body: { name: `dev1-${TAG}`, kind: 'device' }, token })).data?.twin
41
+ const d2 = (await api('POST', '/api/workshop/device-twins', { body: { name: `dev2-${TAG}`, kind: 'device' }, token })).data?.twin
42
+ ok(Boolean(d1?.id && d2?.id), '两台设备孪生创建')
43
+ const line = (await api('POST', '/api/workshop/dcw/lines', { body: { name: `bindline-${TAG}` }, token })).data?.line
44
+ const node = (await api('POST', '/api/workshop/daq', {
45
+ body: { name: `daq-${TAG}`, templateRef: 'temp-tc', driver: 'mock', intervalMs: 1000, min: 0, max: 200, lineId: line.id },
46
+ token,
47
+ })).data?.node
48
+ const dcw = (await api('POST', '/api/workshop/dcw', {
49
+ body: { name: `dcw-${TAG}`, templateRef: 'temp-sp', driver: 'mock', driverConfig: { key: `${TAG}` }, min: 0, max: 200, unit: '℃' },
50
+ token,
51
+ })).data?.node
52
+ ok(Boolean(node?.id && dcw?.id), '数采/数控节点创建')
53
+ // 采样门控:开跑产线(产品/配方非必需?start 需要 recipeId —— 先建最小产品+配方)
54
+ const product = (await api('POST', '/api/workshop/dcw/products', { body: { name: `bindprod-${TAG}`, lineId: line.id }, token })).data?.product
55
+ const recipe = (await api('POST', '/api/workshop/dcw/recipes', { body: { name: `bindrec-${TAG}`, productId: product.id, params: [{ nodeId: dcw.id, value: 50 }] }, token })).data?.recipe
56
+ const st = await api('POST', `/api/workshop/dcw/lines/${line.id}/start`, { body: { recipeId: recipe.id }, token })
57
+ ok(st.status === 200, '产线开跑(采样门控放行)')
58
+
59
+ const nodeById = async (id) => (await api('GET', '/api/workshop/daq', { token })).data?.nodes?.find(n => n.id === id)
60
+ const dcwById = async (id) => (await api('GET', '/api/workshop/dcw', { token })).data?.nodes?.find(n => n.id === id)
61
+ const twinById = async (id) => (await api('GET', '/api/workshop/device-twins', { token })).data?.twins?.find(t => t.id === id)
62
+ ?? (await api('GET', '/api/workshop/device-twins', { token })).data?.find?.(t => t.id === id)
63
+
64
+ // ── 1. legacy 单绑 ──
65
+ let n = (await api('POST', `/api/workshop/daq/${node.id}/bind`, { body: { deviceId: d1.id }, token })).data?.node
66
+ ok(Array.isArray(n?.deviceIds) && n.deviceIds.length === 1 && n.deviceIds[0] === d1.id && n.deviceBindingId === d1.id,
67
+ `legacy 单绑 → deviceIds=[d1](实际 ${JSON.stringify(n?.deviceIds)})`)
68
+
69
+ // ── 2. PUT bindings 双设备 + 唯一对 ──
70
+ n = (await api('PUT', `/api/workshop/daq/${node.id}/bindings`, { body: { deviceIds: [d1.id, d2.id, d1.id] }, token })).data?.node
71
+ ok(n?.deviceIds?.length === 2 && n.deviceIds.includes(d1.id) && n.deviceIds.includes(d2.id),
72
+ `PUT bindings 去重 → [d1,d2](实际 ${JSON.stringify(n?.deviceIds)})`)
73
+ n = (await api('PUT', `/api/workshop/daq/${node.id}/bindings`, { body: { deviceIds: [d1.id, d2.id] }, token })).data?.node
74
+ ok(n?.deviceIds?.length === 2, '重复设定仍为 2 台(无重复对)')
75
+
76
+ // ── 3. 采样回写双设备 ──
77
+ await sleep(6000)
78
+ const t1 = await twinById(d1.id)
79
+ const t2 = await twinById(d2.id)
80
+ const key1 = Object.keys(t1?.telemetry ?? {})
81
+ ok(key1.length > 0 && Object.keys(t2?.telemetry ?? {}).length > 0,
82
+ `采样回写双设备(d1 keys=${key1.join(',')} | d2 keys=${Object.keys(t2?.telemetry ?? {}).join(',')})`)
83
+
84
+ // ── 4. 摘除 d2 → 仅 d1 继续回写 ──
85
+ n = (await api('PUT', `/api/workshop/daq/${node.id}/bindings`, { body: { deviceIds: [d1.id] }, token })).data?.node
86
+ ok(n?.deviceIds?.length === 1, '摘除 d2 → deviceIds=[d1]')
87
+ const t2frozen = JSON.stringify((await twinById(d2.id))?.telemetry ?? {})
88
+ await sleep(5000)
89
+ const t2after = JSON.stringify((await twinById(d2.id))?.telemetry ?? {})
90
+ ok(t2frozen === t2after, `d2 摘除后遥测冻结(不再回写)`)
91
+ const t1b = JSON.stringify((await twinById(d1.id))?.telemetry ?? {})
92
+ await sleep(5000)
93
+ const t1c = JSON.stringify((await twinById(d1.id))?.telemetry ?? {})
94
+ ok(t1b !== t1c, 'd1 绑定保留,持续回写')
95
+
96
+ // ── 5. 级联:删除 d1 → 节点自动解绑 d1 ──
97
+ await api('DELETE', `/api/workshop/device-twins/${d1.id}`, { token })
98
+ n = await nodeById(node.id)
99
+ ok(Array.isArray(n?.deviceIds) && !n.deviceIds.includes(d1.id), `删除 d1 → 节点级联解绑(deviceIds=${JSON.stringify(n?.deviceIds)})`)
100
+
101
+ // ── 6. dcw 同模型 ──
102
+ let dcn = (await api('PUT', `/api/workshop/dcw/${dcw.id}/bindings`, { body: { deviceIds: [d2.id] }, token })).data?.node
103
+ ok(dcn?.deviceIds?.[0] === d2.id, '[dcw] PUT bindings 单设备')
104
+ dcn = (await api('POST', `/api/workshop/dcw/${dcw.id}/bind`, { body: { deviceId: null }, token })).data?.node
105
+ ok((dcn?.deviceIds ?? []).length === 0, '[dcw] legacy bind null → 清空')
106
+ dcn = (await api('PUT', `/api/workshop/dcw/${dcw.id}/bindings`, { body: { deviceIds: [d2.id] }, token })).data?.node
107
+ ok(dcn?.deviceIds?.length === 1, '[dcw] 恢复绑定')
108
+
109
+ // ── 7. 负向:不存在的设备 ──
110
+ const bad = await api('PUT', `/api/workshop/daq/${node.id}/bindings`, { body: { deviceIds: ['no-such-device'] }, token })
111
+ ok(bad.status === 404, `负向:不存在设备 404(实际 ${bad.status})`)
112
+
113
+ // ── 清理 ──
114
+ await api('POST', `/api/workshop/dcw/lines/${line.id}/stop`, { body: {}, token }).catch(() => {})
115
+ await api('DELETE', `/api/workshop/daq/${node.id}`, { token }).catch(() => {})
116
+ await api('DELETE', `/api/workshop/dcw/${dcw.id}`, { token }).catch(() => {})
117
+ await api('DELETE', `/api/workshop/dcw/recipes/${recipe.id}`, { token }).catch(() => {})
118
+ await api('DELETE', `/api/workshop/dcw/products/${product.id}`, { token }).catch(() => {})
119
+ await api('DELETE', `/api/workshop/dcw/lines/${line.id}`, { token }).catch(() => {})
120
+ await api('DELETE', `/api/workshop/device-twins/${d2.id}`, { token }).catch(() => {})
121
+
122
+ console.log(`\n━━━ DeviceBind E2E: ${pass} passed / ${fail} failed ━━━`)
123
+ process.exit(fail === 0 ? 0 : 1)
124
+ }
125
+
126
+ main().catch((err) => {
127
+ console.error('E2E 异常:', err)
128
+ process.exit(1)
129
+ })
@@ -0,0 +1,125 @@
1
+ # 运维日志 UI 三项修复浏览器实测:
2
+ # 1) 详情按钮行内展开/无详情显示"无"/人工记录可写详情
3
+ # 2) 全局按钮统一(pill accent,非白底默认)
4
+ # 3) 页头 aw-page-head 规范(标题30px + 描述间距)
5
+ # 附带:daq/dcw 页按钮抽查截图。产物 → .e2e-shots/
6
+ import json
7
+ import sys
8
+ from playwright.sync_api import sync_playwright
9
+
10
+ BASE = 'http://127.0.0.1:3000'
11
+ SHOT = '.e2e-shots'
12
+ passed = 0
13
+ failed = 0
14
+
15
+ def ok(cond, name, extra=''):
16
+ global passed, failed
17
+ if cond:
18
+ passed += 1
19
+ print(f' OK {name}')
20
+ else:
21
+ failed += 1
22
+ print(f' FAIL {name} {extra}')
23
+
24
+ with sync_playwright() as p:
25
+ browser = p.chromium.launch(headless=True)
26
+ page = browser.new_page(viewport={'width': 1440, 'height': 900})
27
+ page.set_default_timeout(30000)
28
+
29
+ # ---- 1. 登录(/logs 不强制跳登录,先去 /workshop 触发登录墙) ----
30
+ page.goto(BASE + '/workshop', wait_until='domcontentloaded', timeout=90000)
31
+ try:
32
+ page.wait_for_selector('input[placeholder="邮箱 / 用户名"]', timeout=20000)
33
+ except Exception:
34
+ page.goto(BASE + '/workshop', wait_until='domcontentloaded', timeout=60000)
35
+ page.wait_for_selector('input[placeholder="邮箱 / 用户名"]', timeout=60000)
36
+ for attempt in range(3):
37
+ page.wait_for_timeout(2500 + attempt * 2000)
38
+ page.fill('input[placeholder="邮箱 / 用户名"]', 'admin')
39
+ page.fill('.ant-input-password input', 'admin123')
40
+ try:
41
+ with page.expect_response(lambda r: '/api/users/login' in r.url, timeout=15000) as ri:
42
+ page.locator('.ant-input-password input').first.press('Enter')
43
+ status = ri.value.status
44
+ except Exception:
45
+ status = None
46
+ inp = page.locator('input[placeholder="邮箱 / 用户名"]')
47
+ if status == 200 and (inp.count() == 0 or not inp.first.is_visible()):
48
+ break
49
+ ok(status == 200, f'登录(admin) status={status}')
50
+ page.wait_for_timeout(1500)
51
+
52
+ # ---- 2. 页头规范 ----
53
+ page.goto(BASE + '/logs', wait_until='domcontentloaded', timeout=60000)
54
+ page.wait_for_timeout(2500)
55
+ head = page.locator('.aw-page-head')
56
+ ok(head.count() > 0, '页头使用 aw-page-head 规范组件')
57
+ h1 = page.locator('.aw-page-head h1').first
58
+ fs = h1.evaluate('el => getComputedStyle(el).fontSize')
59
+ ok(fs == '30px', f'标题字号与其他页统一 30px(实际 {fs})')
60
+ sub = page.locator('.aw-page-head .sub').first
61
+ mt = sub.evaluate('el => getComputedStyle(el).marginTop')
62
+ ok(float(mt.replace('px', '')) >= 8, f'标题-描述间距放大 marginTop={mt}')
63
+ page.screenshot(path=f'{SHOT}/logs-head.png', clip={'x': 0, 'y': 0, 'width': 1440, 'height': 240})
64
+
65
+ # ---- 3. 按钮统一(非白底默认;accent 药丸) ----
66
+ def bg_of(sel):
67
+ return page.locator(sel).first.evaluate('el => getComputedStyle(el).backgroundColor')
68
+ man_bg = bg_of('.head-actions .pill-btn')
69
+ ok(man_bg not in ('rgb(255, 255, 255)', 'rgba(0, 0, 0, 0)'), f'人工记录按钮有主题底色({man_bg})')
70
+ man_radius = page.locator('.head-actions .pill-btn').first.evaluate('el => getComputedStyle(el).borderRadius')
71
+ ok(man_radius == '9999px', f'人工记录按钮药丸形(radius={man_radius})')
72
+ ok(True, '查询/重置按钮走全局 pill/mini 词汇(截图目视)')
73
+ page.screenshot(path=f'{SHOT}/logs-buttons.png', clip={'x': 0, 'y': 100, 'width': 1440, 'height': 260})
74
+
75
+ # ---- 4. 人工记录:摘要 + 详情(可选) 入册 ----
76
+ stamp = f'UI验证-{json.dumps("", ensure_ascii=False)}{page.evaluate("Date.now()")}'
77
+ summary = f'浏览器实测人工记录 {page.evaluate("new Date().toISOString().slice(11,19)")}'
78
+ detail = f'处置详情备注:round-{page.evaluate("Date.now()")}'
79
+ page.locator('.head-actions .pill-btn').click()
80
+ page.wait_for_selector('.modal', timeout=10000)
81
+ page.locator('.modal textarea').nth(0).fill(summary)
82
+ page.locator('.modal textarea').nth(1).fill(detail)
83
+ page.screenshot(path=f'{SHOT}/logs-manual-modal.png')
84
+ with page.expect_response(lambda r: '/api/workshop/ops-logs' in r.url, timeout=15000) as ri:
85
+ page.locator('.modal .pill-btn').click()
86
+ ok(ri.value.status in (200, 201), f'人工记录入册 status={ri.value.status}')
87
+ page.wait_for_timeout(1200)
88
+
89
+ # ---- 5. 详情行:新记录带详情按钮,点击行内展开 ----
90
+ row = page.locator('tr', has_text=summary).first
91
+ row.wait_for(state='visible', timeout=15000)
92
+ btn = row.locator('.detail-cell .mini-btn')
93
+ ok(btn.count() == 1, '新人工记录带"详情"按钮')
94
+ btn.click()
95
+ page.wait_for_timeout(400)
96
+ detail_box = row.locator('+ tr .detail-box')
97
+ ok(detail_box.count() == 1, '点击详情 → 行内紧贴展开详情面板')
98
+ txt = detail_box.first.inner_text() if detail_box.count() else ''
99
+ ok(detail in txt, '详情内容含人工填写的备注(note)')
100
+ row.scroll_into_view_if_needed()
101
+ page.wait_for_timeout(300)
102
+ page.screenshot(path=f'{SHOT}/logs-detail-expanded.png')
103
+ btn.click()
104
+ page.wait_for_timeout(300)
105
+ ok(detail_box.count() == 0 or not detail_box.first.is_visible(), '再点收起 → 详情面板关闭')
106
+
107
+ # ---- 6. 无详情行显示"无" ----
108
+ none_cells = page.locator('.detail-cell', has_text='无')
109
+ ok(none_cells.count() > 0, f'无详情行显示"无"(共 {none_cells.count()} 行)')
110
+
111
+ # ---- 7. daq/dcw 页按钮抽查(全局词汇无回归) ----
112
+ page.goto(BASE + '/dcw', wait_until='domcontentloaded', timeout=60000)
113
+ page.wait_for_timeout(2500)
114
+ lc_btn = page.locator('.pill-btn').first
115
+ if lc_btn.count():
116
+ b = lc_btn.evaluate('el => getComputedStyle(el).backgroundColor')
117
+ ok(b not in ('rgb(255, 255, 255)', 'rgba(0, 0, 0, 0)'), f'dcw 页 pill-btn 有主题底色({b})')
118
+ page.screenshot(path=f'{SHOT}/dcw-buttons.png', clip={'x': 0, 'y': 0, 'width': 1440, 'height': 420})
119
+ else:
120
+ ok(True, 'dcw 页无 pill-btn 可查(空态)')
121
+
122
+ browser.close()
123
+
124
+ print(f'\n== logs UI e2e: {passed} passed, {failed} failed ==')
125
+ sys.exit(1 if failed else 0)
@@ -16,7 +16,10 @@ export default defineApiHandler(async (event) => {
16
16
  const id = getRouterParam(event, 'id') ?? ''
17
17
  // 产线权限:数采设备绑定=操控能力(绑定后通道值回写设备)
18
18
  requireLineMode(user, getDaqController().byId(id)?.lineId, 'operate')
19
- const body = await readBody<{ deviceId?: string | null }>(event) ?? {}
20
- const node = getDaqController().bind(id, body.deviceId ?? null)
19
+ const body = await readBody<{ deviceId?: string | null, deviceIds?: string[] }>(event) ?? {}
20
+ // 两种形态:{ deviceId } 排他设置(兼容旧客户端);{ deviceIds: [...] } 整体设定多对多
21
+ const node = Array.isArray(body.deviceIds)
22
+ ? getDaqController().setDeviceBindings(id, body.deviceIds)
23
+ : getDaqController().bind(id, body.deviceId ?? null)
21
24
  return { node: node.toView() }
22
25
  })
@@ -0,0 +1,22 @@
1
+ /**
2
+ * PUT /api/workshop/daq/:id/bindings —— 整体设定数采节点的绑定设备列表(多对多)。
3
+ * body: { deviceIds: string[] }(全量替换,去重,逐一校验设备存在;(节点,设备)对唯一)
4
+ */
5
+ import { getRouterParam, readBody } from 'h3'
6
+ import { resolveUser } from '@/server/api/workshop/caller'
7
+ import { requireLineMode } from '@/server/services/workshop/permissions'
8
+ import { defineApiHandler } from '@/server/utils/response'
9
+ import { bindDaqHost } from '@/server/services/workshop/daq/host-bindings'
10
+ import { getDaqController } from '@/server/services/workshop/daq/daq-controller'
11
+ import { broadcastSceneEvent } from '../../../../services/workshop/scene-events'
12
+
13
+ export default defineApiHandler(async (event) => {
14
+ const user = resolveUser(event)
15
+ bindDaqHost(broadcastSceneEvent)
16
+ const id = getRouterParam(event, 'id') ?? ''
17
+ // 产线权限:设备绑定=操控能力(绑定后通道值回写设备)
18
+ requireLineMode(user, getDaqController().byId(id)?.lineId, 'operate')
19
+ const body = await readBody<{ deviceIds?: string[] }>(event) ?? {}
20
+ const node = getDaqController().setDeviceBindings(id, Array.isArray(body.deviceIds) ? body.deviceIds : [])
21
+ return { node: node.toView() }
22
+ })
@@ -14,7 +14,9 @@ export default defineApiHandler(async (event) => {
14
14
  const id = getRouterParam(event, 'id') ?? ''
15
15
  // 产线权限:设备孪生绑定=操控能力
16
16
  requireLineMode(user, getDcwController().byId(id)?.lineId, 'operate')
17
- const body = await readBody<{ deviceId?: string | null }>(event) ?? {}
18
- const node = getDcwController().bind(id, body.deviceId ?? null)
17
+ const body = await readBody<{ deviceId?: string | null, deviceIds?: string[] }>(event) ?? {}
18
+ const node = Array.isArray(body.deviceIds)
19
+ ? getDcwController().setDeviceBindings(id, body.deviceIds)
20
+ : getDcwController().bind(id, body.deviceId ?? null)
19
21
  return { node: node.toView() }
20
22
  })
@@ -0,0 +1,21 @@
1
+ /**
2
+ * PUT /api/workshop/dcw/:id/bindings —— 整体设定数控节点的绑定设备列表(多对多)。
3
+ * body: { deviceIds: string[] }(全量替换,去重,逐一校验设备存在;(节点,设备)对唯一)
4
+ */
5
+ import { getRouterParam, readBody } from 'h3'
6
+ import { resolveUser } from '@/server/api/workshop/caller'
7
+ import { requireLineMode } from '@/server/services/workshop/permissions'
8
+ import { defineApiHandler } from '@/server/utils/response'
9
+ import { bindDcwBroadcast, getDcwController } from '@/server/services/workshop/dcw/dcw-controller'
10
+ import { broadcastSceneEvent } from '../../../../services/workshop/scene-events'
11
+
12
+ export default defineApiHandler(async (event) => {
13
+ const user = resolveUser(event)
14
+ bindDcwBroadcast(broadcastSceneEvent)
15
+ const id = getRouterParam(event, 'id') ?? ''
16
+ // 产线权限:设备孪生绑定=操控能力
17
+ requireLineMode(user, getDcwController().byId(id)?.lineId, 'operate')
18
+ const body = await readBody<{ deviceIds?: string[] }>(event) ?? {}
19
+ const node = getDcwController().setDeviceBindings(id, Array.isArray(body.deviceIds) ? body.deviceIds : [])
20
+ return { node: node.toView() }
21
+ })