agentworkshop 0.7.21 → 0.7.22

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 (82) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/{BA3XzwiD.js → 0exC4jVL.js} +1 -1
  3. package/.output/public/_nuxt/BAbAEW0D.js +1 -0
  4. package/.output/public/_nuxt/{DBXhDLgl.js → BuiGDfMB.js} +1 -1
  5. package/.output/public/_nuxt/Ci0uX4c4.js +1 -0
  6. package/.output/public/_nuxt/D6B0xnjk.js +1 -0
  7. package/.output/public/_nuxt/{TownView.BHLKAjEu.css → TownView.zu2rU5nS.css} +1 -1
  8. package/.output/public/_nuxt/_id_.C2VHq_vT.css +1 -0
  9. package/.output/public/_nuxt/_id_.Dswy2p0I.css +1 -0
  10. package/.output/public/_nuxt/builds/latest.json +1 -1
  11. package/.output/public/_nuxt/builds/meta/e2341fc0-6f3f-4680-abf8-935a7c9fb912.json +1 -0
  12. package/.output/public/_nuxt/{BOJaHO5V.js → c3mwkHWP.js} +2 -2
  13. package/.output/public/_nuxt/daq.BTdEun-8.css +1 -0
  14. package/.output/public/_nuxt/dbi5bHkY.js +2 -0
  15. package/.output/public/_nuxt/{DsOkkMX3.js → jyQyulh-.js} +1 -1
  16. package/.output/public/_nuxt/{B-E8fXAO.js → n3vMFfrv.js} +1 -1
  17. package/.output/server/chunks/_/nitro.mjs +873 -748
  18. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  19. package/.output/server/chunks/build/{TownView-ql2eHZOg.mjs → TownView-DxeQe1Bl.mjs} +144 -143
  20. package/.output/server/chunks/build/{TownView-ql2eHZOg.mjs.map → TownView-DxeQe1Bl.mjs.map} +1 -1
  21. package/.output/server/chunks/build/{_id_-CwR5oLu9.mjs → _id_-B9RvEQFC.mjs} +33 -35
  22. package/.output/server/chunks/build/_id_-B9RvEQFC.mjs.map +1 -0
  23. package/.output/server/chunks/build/{_id_-Bs-euJPu.mjs → _id_-CqF0R5W5.mjs} +138 -137
  24. package/.output/server/chunks/build/_id_-CqF0R5W5.mjs.map +1 -0
  25. package/.output/server/chunks/build/{_wsId_-B3aB9bIp.mjs → _wsId_-YC2t_fRt.mjs} +3 -3
  26. package/.output/server/chunks/build/{_wsId_-B3aB9bIp.mjs.map → _wsId_-YC2t_fRt.mjs.map} +1 -1
  27. package/.output/server/chunks/build/{daq-epWL_sC7.mjs → daq-Bhwwz9vp.mjs} +146 -118
  28. package/.output/server/chunks/build/daq-Bhwwz9vp.mjs.map +1 -0
  29. package/.output/server/chunks/build/{pages-Dq7_H4FK.mjs → pages-D9tOoJsf.mjs} +2 -2
  30. package/.output/server/chunks/build/{pages-Dq7_H4FK.mjs.map → pages-D9tOoJsf.mjs.map} +1 -1
  31. package/.output/server/chunks/build/{town-CstiKxf_.mjs → town-Dsh0jlwZ.mjs} +3 -3
  32. package/.output/server/chunks/build/{town-CstiKxf_.mjs.map → town-Dsh0jlwZ.mjs.map} +1 -1
  33. package/.output/server/chunks/build/{useDaqStream-BOOZ-p-l.mjs → useDaqStream-DkRL3tEW.mjs} +9 -1
  34. package/.output/server/chunks/build/{useDaqStream-BOOZ-p-l.mjs.map → useDaqStream-DkRL3tEW.mjs.map} +1 -1
  35. package/.output/server/chunks/routes/api/workshop/daq/_id/bind.post.mjs +1 -1
  36. package/.output/server/chunks/routes/api/workshop/daq/_id/bind.post.mjs.map +1 -1
  37. package/.output/server/chunks/routes/api/workshop/daq/_id/bindings.put.mjs +34 -0
  38. package/.output/server/chunks/routes/api/workshop/daq/_id/bindings.put.mjs.map +1 -0
  39. package/.output/server/chunks/routes/api/workshop/dcw/_id/bind.post.mjs +1 -1
  40. package/.output/server/chunks/routes/api/workshop/dcw/_id/bind.post.mjs.map +1 -1
  41. package/.output/server/chunks/routes/api/workshop/dcw/_id/bindings.put.mjs +33 -0
  42. package/.output/server/chunks/routes/api/workshop/dcw/_id/bindings.put.mjs.map +1 -0
  43. package/.output/server/chunks/routes/renderer.mjs +1 -1
  44. package/.output/server/chunks/virtual/entry.mjs +6 -6
  45. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  46. package/.output/server/package.json +1 -1
  47. package/app/components/workshop/town/TownView.vue +7082 -7076
  48. package/app/composables/workshop/useDaqStream.ts +10 -0
  49. package/app/pages/daq/[id].vue +4 -3
  50. package/app/pages/daq/index.vue +66 -21
  51. package/app/pages/dcw/[id].vue +5 -1
  52. package/package.json +148 -148
  53. package/scripts/_dbg-device-bind-e2e.mjs +129 -0
  54. package/server/api/workshop/daq/[id]/bind.post.ts +5 -2
  55. package/server/api/workshop/daq/[id]/bindings.put.ts +22 -0
  56. package/server/api/workshop/dcw/[id]/bind.post.ts +4 -2
  57. package/server/api/workshop/dcw/[id]/bindings.put.ts +21 -0
  58. package/server/data/daqs.json +408 -103
  59. package/server/data/dcw-lines.json +7 -0
  60. package/server/data/dcw-rollback.json +10 -10
  61. package/server/data/dcw-runs.json +25 -27
  62. package/server/data/dcw-writes.json +12 -0
  63. package/server/data/dcws.json +216 -216
  64. package/server/data/line-runs.json +18 -18
  65. package/server/services/workshop/agents/industrial-context.ts +231 -222
  66. package/server/services/workshop/daq/daq-controller.ts +76 -23
  67. package/server/services/workshop/daq/daq-node.ts +11 -1
  68. package/server/services/workshop/dcw/dcw-controller.ts +50 -2
  69. package/server/services/workshop/dcw/dcw-node.ts +10 -1
  70. package/shared/daq-protocol.ts +2 -0
  71. package/shared/dcw-protocol.ts +2 -0
  72. package/.output/public/_nuxt/B_C3GLsD.js +0 -1
  73. package/.output/public/_nuxt/Ba7YofCb.js +0 -2
  74. package/.output/public/_nuxt/CgT5wMfz.js +0 -1
  75. package/.output/public/_nuxt/D4CLp1hc.js +0 -1
  76. package/.output/public/_nuxt/_id_.DIxEKj88.css +0 -1
  77. package/.output/public/_nuxt/_id_.IbmpkhH4.css +0 -1
  78. package/.output/public/_nuxt/builds/meta/5d344836-03a5-4f47-a0a7-fd30192f5a8f.json +0 -1
  79. package/.output/public/_nuxt/daq.6IrhVVfy.css +0 -1
  80. package/.output/server/chunks/build/_id_-Bs-euJPu.mjs.map +0 -1
  81. package/.output/server/chunks/build/_id_-CwR5oLu9.mjs.map +0 -1
  82. package/.output/server/chunks/build/daq-epWL_sC7.mjs.map +0 -1
@@ -308,6 +308,15 @@ const createStore = () => {
308
308
  upsert(data.node)
309
309
  }
310
310
 
311
+ /** 多对多绑定:整体设定节点的绑定设备列表(服务端去重+校验) */
312
+ async function setNodeBindings(id: string, deviceIds: string[]): Promise<void> {
313
+ const data = await api<{ node: DaqNodeView }>(`/${id}/bindings`, {
314
+ method: 'PUT',
315
+ body: JSON.stringify({ deviceIds }),
316
+ })
317
+ upsert(data.node)
318
+ }
319
+
311
320
  async function testDriver(driver: string, driverConfig: Record<string, string | number | boolean>): Promise<DaqDriverTestResult> {
312
321
  const data = await api<{ test: DaqDriverTestResult }>('/test-driver', {
313
322
  method: 'POST',
@@ -422,6 +431,7 @@ const createStore = () => {
422
431
  saveTransform,
423
432
  removeNode,
424
433
  bindNode,
434
+ setNodeBindings,
425
435
  controllerAction,
426
436
  alarms,
427
437
  fetchAlarms,
@@ -197,9 +197,10 @@ function driverPlanned(kind: string): boolean {
197
197
  // ---------- 设备绑定 ----------
198
198
  const bindDeviceId = ref('')
199
199
  const boundDeviceName = computed(() => {
200
- const id = node.value?.deviceBindingId
201
- if (!id) return ''
202
- return deviceTwins.twins.find(t => t.id === id)?.name ?? `${id.slice(0, 8)}…`
200
+ const ids = node.value?.deviceIds ?? (node.value?.deviceBindingId ? [node.value.deviceBindingId] : [])
201
+ return ids
202
+ .map(id => deviceTwins.twins.find(t => t.id === id)?.name ?? `${id.slice(0, 8)}…`)
203
+ .join(' / ')
203
204
  })
204
205
  function onBindToggle(): void {
205
206
  void daq.bindNode(nodeId.value, node.value?.deviceBindingId ? null : (bindDeviceId.value || null))
@@ -40,17 +40,38 @@ async function setNodeLine(id: string, e: Event): Promise<void> {
40
40
  }
41
41
  }
42
42
 
43
- /** 节点绑定设备(bind REST server 落库;与数字孪生/节点检查器同一条 deviceBindingId 链路) */
44
- async function setNodeDevice(id: string, e: Event): Promise<void> {
43
+ /** 节点绑定设备(多对多:add 挂新设备 / remove 摘除一台;server 落库,与数字孪生同一 deviceIds 链路) */
44
+ async function addNodeDevice(id: string, e: Event): Promise<void> {
45
45
  const deviceId = (e.target as HTMLSelectElement).value
46
+ if (!deviceId) return
47
+ const node = daq.nodes.find(n => n.id === id)
48
+ if (!node) return
49
+ if (node.deviceIds?.includes(deviceId)) return
46
50
  try {
47
- await daq.bindNode(id, deviceId || null)
51
+ await daq.setNodeBindings(id, [...(node.deviceIds ?? []), deviceId])
48
52
  }
49
53
  catch (err) {
50
54
  message.error(apiErrorMessage(err))
51
55
  }
56
+ ;(e.target as HTMLSelectElement).value = ''
52
57
  }
53
58
 
59
+ async function removeNodeDevice(id: string, deviceId: string): Promise<void> {
60
+ const node = daq.nodes.find(n => n.id === id)
61
+ if (!node) return
62
+ try {
63
+ await daq.setNodeBindings(id, (node.deviceIds ?? []).filter(d => d !== deviceId))
64
+ }
65
+ catch (err) {
66
+ message.error(apiErrorMessage(err))
67
+ }
68
+ }
69
+
70
+ const nodeDeviceIds = (n: DaqNodeView): string[] =>
71
+ n.deviceIds ?? (n.deviceBindingId ? [n.deviceBindingId] : [])
72
+ const deviceNameById = (id: string): string =>
73
+ nodeDevices.get(id) ?? deviceTwins.twins.find(t => t.id === id)?.name ?? id
74
+
54
75
  /** 单节点独立 启动/停止采集(PATCH enabled;server 权威,本地乐观翻转 + WS/轮询收敛) */
55
76
  async function toggleNodeEnabled(n: { id: string, enabled: boolean, state: string }): Promise<void> {
56
77
  const next = !n.enabled
@@ -295,7 +316,9 @@ function fmtPoint(v: number | null | undefined): string {
295
316
  const boundDevices = computed<Array<{ id: string, name: string, count: number }>>(() => {
296
317
  const used = new Map<string, number>()
297
318
  for (const n of daq.nodes) {
298
- if (n.deviceBindingId) used.set(n.deviceBindingId, (used.get(n.deviceBindingId) ?? 0) + 1)
319
+ for (const d of n.deviceIds ?? (n.deviceBindingId ? [n.deviceBindingId] : [])) {
320
+ used.set(d, (used.get(d) ?? 0) + 1)
321
+ }
299
322
  }
300
323
  return deviceTwins.twins
301
324
  .filter(t => used.has(t.id))
@@ -371,7 +394,8 @@ function statePillOf(n: DaqNodeView): { key: RowState, label: string, tip: strin
371
394
 
372
395
  const filteredNodes = computed<DaqNodeView[]>(() => daq.nodes.filter((n) => {
373
396
  if (filters.lineId && (filters.lineId === 'none' ? !!n.lineId : (n.lineId ?? '') !== filters.lineId)) return false
374
- if (filters.deviceId && (filters.deviceId === 'none' ? !!n.deviceBindingId : n.deviceBindingId !== filters.deviceId)) return false
397
+ const nodeDevIds = n.deviceIds ?? (n.deviceBindingId ? [n.deviceBindingId] : [])
398
+ if (filters.deviceId && (filters.deviceId === 'none' ? nodeDevIds.length > 0 : !nodeDevIds.includes(filters.deviceId))) return false
375
399
  if (filters.template && n.templateRef !== filters.template) return false
376
400
  if (filters.driver && n.driver !== filters.driver) return false
377
401
  if (!lineRunMatch(n)) return false
@@ -1771,23 +1795,39 @@ async function doReconnect(): Promise<void> {
1771
1795
  >--</span>
1772
1796
  </td>
1773
1797
  <td>
1774
- <select
1775
- class="line-sel"
1776
- :value="n.deviceBindingId ?? ''"
1777
- :title="$t('daq.k2bindtip137')"
1778
- @change="setNodeDevice(n.id, $event)"
1779
- >
1780
- <option value="">
1781
- {{ $t('daq.k3own4q056') }}
1782
- </option>
1783
- <option
1784
- v-for="d in bindableDeviceTwins"
1785
- :key="d.id"
1786
- :value="d.id"
1798
+ <div class="dev-binds">
1799
+ <span
1800
+ v-for="did in nodeDeviceIds(n)"
1801
+ :key="did"
1802
+ class="dev-bind-chip"
1803
+ :title="$t('daq.k2bindtip137')"
1787
1804
  >
1788
- {{ nodeDevices.get(d.id) ?? d.name }}
1789
- </option>
1790
- </select>
1805
+ {{ deviceNameById(did) }}
1806
+ <span
1807
+ class="dev-unbind"
1808
+ title="unbind"
1809
+ @click="removeNodeDevice(n.id, did)"
1810
+ >✕</span>
1811
+ </span>
1812
+ <select
1813
+ class="line-sel dev-add"
1814
+ :value="''"
1815
+ :title="$t('daq.k2bindtip137')"
1816
+ @change="addNodeDevice(n.id, $event)"
1817
+ >
1818
+ <option value="">
1819
+ {{ $t('daq.k3own4q056') }}
1820
+ </option>
1821
+ <option
1822
+ v-for="d in bindableDeviceTwins"
1823
+ :key="d.id"
1824
+ :value="d.id"
1825
+ :disabled="(n.deviceIds ?? []).includes(d.id)"
1826
+ >
1827
+ {{ nodeDevices.get(d.id) ?? d.name }}
1828
+ </option>
1829
+ </select>
1830
+ </div>
1791
1831
  </td>
1792
1832
  <td class="right">
1793
1833
  <button
@@ -2395,6 +2435,11 @@ tr.row-recipe-alarm td:first-child { box-shadow: inset 3px 0 0 var(--tone-danger
2395
2435
  .flt-search::placeholder { color: var(--ink-fainter); }
2396
2436
  .count { padding-bottom: 4px; font-size: 11px; color: var(--ink-faint); }
2397
2437
 
2438
+ .dev-binds { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
2439
+ .dev-bind-chip { display: inline-flex; gap: 4px; align-items: center; padding: 1px 7px; font-size: 11px; color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent); border-radius: var(--radius-chip); }
2440
+ .dev-unbind { cursor: pointer; opacity: 0.55; font-size: 10px; }
2441
+ .dev-unbind:hover { opacity: 1; color: var(--tone-danger-dot); }
2442
+ .dev-add { max-width: 130px; font-size: 11px; }
2398
2443
  .line-sel {
2399
2444
  max-width: 120px;
2400
2445
  padding: 3px 6px;
@@ -25,6 +25,10 @@ void dcw.load()
25
25
  const unsubDcw = dcw.ensureWsFeed()
26
26
  onUnmounted(() => unsubDcw())
27
27
 
28
+ const nodeDeviceNames = (n: { deviceIds?: string[], deviceBindingId?: string | null }): string =>
29
+ (n.deviceIds ?? (n.deviceBindingId ? [n.deviceBindingId] : []))
30
+ .map(id => deviceName(id))
31
+ .join(' / ') || '—'
28
32
  const deviceName = (id: string | null): string =>
29
33
  id ? (deviceTwins.twins.find(t => t.id === id)?.name ?? id) : t('dcwDetail.k3own4q121')
30
34
 
@@ -1341,7 +1345,7 @@ function fmtPoint(p: { value?: number, avg?: number } | undefined): string {
1341
1345
  :title="$t('dcwDetail.k9r7d4e016')"
1342
1346
  >· {{ n.readIntervalMs === 0 ? $t('dcwDetail.k9r7d4e022') : (n.readIntervalMs == null ? $t('dcwDetail.k9r7d4e023') : `${n.readIntervalMs}ms`) }}</small>
1343
1347
  </td>
1344
- <td>{{ deviceName(n.deviceBindingId) }}</td>
1348
+ <td>{{ nodeDeviceNames(n) }}</td>
1345
1349
  <td class="right">
1346
1350
  <button
1347
1351
  class="mini-btn danger"
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.22",
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
+ })
@@ -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
  })