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
@@ -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
@@ -2185,7 +2225,7 @@ tr.row-recipe-alarm td:first-child { box-shadow: inset 3px 0 0 var(--tone-danger
2185
2225
  .infra-banner .txt a { color: var(--accent); text-decoration: underline; }
2186
2226
  .infra-banner .txt b { font-weight: 600; }
2187
2227
  .infra-banner .txt small { margin-left: 8px; font-size: 10.5px; opacity: 0.75; }
2188
- .infra-banner .pill-btn { flex: 0 0 auto; color: var(--paper-raised); text-decoration: none; }
2228
+ .infra-banner .pill-btn { flex: 0 0 auto; }
2189
2229
 
2190
2230
  /* ---------- 未确认报警条(S5) ---------- */
2191
2231
  .alarm-bar {
@@ -2311,16 +2351,6 @@ tr.row-recipe-alarm td:first-child { box-shadow: inset 3px 0 0 var(--tone-danger
2311
2351
  }
2312
2352
  .hint { font-size: 10px; color: var(--ink-fainter); }
2313
2353
  .test-row { display: flex; gap: 10px; align-items: center; margin: 6px 0 4px; }
2314
- .pill-btn {
2315
- padding: 7px 14px;
2316
- font-size: 12.5px;
2317
- cursor: pointer;
2318
- color: var(--paper-raised);
2319
- background: var(--accent);
2320
- border: 1px solid var(--accent);
2321
- border-radius: var(--radius-pill);
2322
- }
2323
- .pill-btn:disabled { opacity: 0.5; cursor: default; }
2324
2354
  .test-result { font-family: var(--font-mono); font-size: 11px; }
2325
2355
  .test-result.ok { color: var(--tone-success-dot); }
2326
2356
  .test-result.bad { color: var(--tone-danger-dot); }
@@ -2343,17 +2373,6 @@ tr.row-recipe-alarm td:first-child { box-shadow: inset 3px 0 0 var(--tone-danger
2343
2373
  .tpl-table .range { font-size: 11.5px; color: var(--ink-soft); }
2344
2374
  .tpl-table .empty { color: var(--ink-fainter); font-size: 12px; text-align: center; padding: 14px 0; }
2345
2375
  .tpl-table .actions { white-space: nowrap; }
2346
- .mini-btn {
2347
- padding: 3px 10px;
2348
- font-size: 11.5px;
2349
- cursor: pointer;
2350
- color: var(--ink-soft);
2351
- background: var(--paper-deep);
2352
- border: 1px solid var(--line-strong);
2353
- border-radius: var(--radius-chip);
2354
- }
2355
- .mini-btn.danger { color: var(--tone-danger-dot); }
2356
- .mini-btn:hover { border-color: var(--accent); color: var(--accent); }
2357
2376
 
2358
2377
  /* ---------- 节点筛选工具条 ---------- */
2359
2378
  .tbl-toolbar {
@@ -2395,6 +2414,11 @@ tr.row-recipe-alarm td:first-child { box-shadow: inset 3px 0 0 var(--tone-danger
2395
2414
  .flt-search::placeholder { color: var(--ink-fainter); }
2396
2415
  .count { padding-bottom: 4px; font-size: 11px; color: var(--ink-faint); }
2397
2416
 
2417
+ .dev-binds { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
2418
+ .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); }
2419
+ .dev-unbind { cursor: pointer; opacity: 0.55; font-size: 10px; }
2420
+ .dev-unbind:hover { opacity: 1; color: var(--tone-danger-dot); }
2421
+ .dev-add { max-width: 130px; font-size: 11px; }
2398
2422
  .line-sel {
2399
2423
  max-width: 120px;
2400
2424
  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"
@@ -2439,7 +2443,7 @@ h1 { margin: 2px 0 4px; font-size: 30px; font-weight: 400; letter-spacing: -0.01
2439
2443
  .write-row { display: flex; gap: 6px; align-items: center; }
2440
2444
  .write-inp { width: 92px; padding: 4px 8px; }
2441
2445
  .write-btn { padding: 5px 12px; }
2442
- .mini-btn { padding: 4px 10px; font-size: 11.5px; cursor: pointer; color: var(--ink-soft); background: var(--paper-deep); border: 1px solid var(--line-strong); border-radius: var(--radius-chip); margin-right: 4px; }
2446
+ .mini-btn { margin-right: 4px; }
2443
2447
  .mini-btn.danger { color: var(--tone-danger-dot); }
2444
2448
  .mini-btn:hover { border-color: var(--accent); color: var(--accent); }
2445
2449
  .inp { padding: 5px 9px; font-size: 12.5px; color: var(--ink); background: var(--paper-deep); border: 1px solid var(--line-strong); border-radius: var(--radius-chip); }
@@ -2493,9 +2497,6 @@ h1 { margin: 2px 0 4px; font-size: 30px; font-weight: 400; letter-spacing: -0.01
2493
2497
  .ver-diff { margin: 4px 0 0; font-size: 11px; color: var(--ink-faint); }
2494
2498
  .sec-label { margin: 14px 0 6px; font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-faint); }
2495
2499
  .runs { margin-top: 6px; }
2496
- .pill-btn { padding: 6px 14px; font-size: 12.5px; cursor: pointer; color: var(--paper-raised); background: var(--accent); border: 1px solid var(--accent); border-radius: var(--radius-pill); }
2497
- .pill-btn.outline { color: var(--ink); background: var(--paper-raised); border-color: var(--line-strong); }
2498
- .pill-btn:disabled { opacity: 0.5; cursor: default; }
2499
2500
 
2500
2501
  .modal-mask { position: fixed; z-index: 50; inset: 0; display: flex; align-items: center; justify-content: center; background: var(--scrim); backdrop-filter: blur(2px); }
2501
2502
  .modal { width: 680px; max-width: 94vw; max-height: 88vh; overflow-y: auto; padding: 22px 24px; background: var(--surface-glass-strong); backdrop-filter: var(--aurora-blur); -webkit-backdrop-filter: var(--aurora-blur); border: 1px solid var(--glass-line); border-radius: var(--radius-panel); box-shadow: var(--glass-edge), var(--shadow-float); }
@@ -925,25 +925,7 @@ const builtinCount = computed(() => dcw.templates.filter(t => t.builtin).length)
925
925
  border-radius: var(--radius-panel-sm);
926
926
  }
927
927
  .lc-ctl .inp:focus { outline: none; border-color: var(--tone-success-dot); box-shadow: 0 0 0 3px var(--accent-soft); }
928
- .pill-btn {
929
- flex: none;
930
- height: 30px;
931
- padding: 0 14px;
932
- font-size: 11.5px;
933
- font-weight: 600;
934
- color: var(--on-accent);
935
- background: var(--accent);
936
- border: 0;
937
- border-radius: var(--radius-panel-sm);
938
- cursor: pointer;
939
- transition: background 0.15s, box-shadow 0.15s;
940
- }
941
- .pill-btn:hover:not(:disabled) { background: var(--accent-strong); box-shadow: 0 0 14px var(--accent-soft); }
942
- .pill-btn:disabled { opacity: 0.4; cursor: default; }
943
- .pill-btn.danger,
944
- .pill-btn.stop { background: var(--tone-danger-bg); color: var(--tone-danger-dot); }
945
- .pill-btn.danger:hover:not(:disabled),
946
- .pill-btn.stop:hover:not(:disabled) { background: color-mix(in srgb, var(--tone-danger-dot) 26%, transparent); box-shadow: none; }
928
+ .lc-ctl .pill-btn { flex: none; }
947
929
  .lc-manage {
948
930
  font-size: 11.5px;
949
931
  font-weight: 600;
@@ -1065,15 +1047,4 @@ const builtinCount = computed(() => dcw.templates.filter(t => t.builtin).length)
1065
1047
  .tpl-form { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
1066
1048
  .tpl-sem { flex: 1; color: var(--ink-faint); }
1067
1049
  textarea.inp { height: auto; padding: 6px 9px; font-size: 11.5px; resize: vertical; }
1068
- .mini-btn {
1069
- padding: 4px 10px;
1070
- font-size: 10.5px;
1071
- color: var(--ink-faint);
1072
- background: var(--paper-deep);
1073
- border: 1px solid var(--line-strong);
1074
- border-radius: 7px;
1075
- cursor: pointer;
1076
- }
1077
- .mini-btn:hover { border-color: var(--ink-faint); color: var(--ink); }
1078
- .mini-btn.danger { color: var(--tone-danger-dot); }
1079
1050
  </style>
@@ -1,14 +1,12 @@
1
1
  <template>
2
2
  <div class="page">
3
- <!-- 页头:标题 + 手动记录入口 -->
4
- <header class="head">
3
+ <!-- 页头(aw-page-head 规范:kicker + 大标题 + 描述)+ 手动记录入口 -->
4
+ <header class="aw-page-head">
5
5
  <div>
6
6
  <p class="aw-kicker">
7
7
  agentworkshop / audit log
8
8
  </p>
9
- <h1 class="h1">
10
- {{ $t('logs.title') }}
11
- </h1>
9
+ <h1>{{ $t('logs.title') }}</h1>
12
10
  <p class="sub">
13
11
  {{ $t('logs.sub') }}
14
12
  </p>
@@ -182,67 +180,78 @@
182
180
  </tr>
183
181
  </thead>
184
182
  <tbody>
185
- <tr
183
+ <template
186
184
  v-for="row in opsLog.results"
187
- :key="`${row.id}-${row.at}`"
185
+ :key="row.id"
188
186
  >
189
- <td class="mono dim">
190
- {{ fmtTime(row.at) }}
191
- </td>
192
- <td>
193
- <span
194
- class="src-badge"
195
- :class="row.actorKind"
196
- >{{ srcLabel(row.actorKind) }}</span>
197
- </td>
198
- <td class="actor">
199
- {{ row.actorName || row.actor || '—' }}
200
- </td>
201
- <td>
202
- <span
203
- class="kind-chip"
204
- :class="row.kind"
205
- >{{ kindLabel(row.kind) }}</span>
206
- </td>
207
- <td class="summary">
208
- {{ row.summary }}
209
- <small class="mono dim action">{{ row.action }}</small>
210
- </td>
211
- <td class="scope">
212
- <span
213
- v-if="lineName(row.lineId)"
214
- class="scope-chip"
215
- :title="$t('logs.fLine')"
216
- >{{ lineName(row.lineId) }}</span>
217
- <span
218
- v-if="productName(row.productId)"
219
- class="scope-chip"
220
- :title="$t('logs.fProduct')"
221
- >{{ productName(row.productId) }}</span>
222
- <span
223
- v-if="recipeName(row.recipeId)"
224
- class="scope-chip"
225
- :title="'Recipe'"
226
- >{{ recipeName(row.recipeId) }}</span>
227
- <span
228
- v-if="!row.lineId && !row.productId && !row.recipeId"
229
- class="dim"
230
- >—</span>
231
- </td>
232
- <td class="detail-cell">
233
- <button
234
- v-if="row.detailJson && row.detailJson !== '{}'"
235
- class="mini-btn"
236
- @click="toggle(row)"
187
+ <tr>
188
+ <td class="mono dim">
189
+ {{ fmtTime(row.at) }}
190
+ </td>
191
+ <td>
192
+ <span
193
+ class="src-badge"
194
+ :class="row.actorKind"
195
+ >{{ srcLabel(row.actorKind) }}</span>
196
+ </td>
197
+ <td class="actor">
198
+ {{ row.actorName || row.actor || '—' }}
199
+ </td>
200
+ <td>
201
+ <span
202
+ class="kind-chip"
203
+ :class="row.kind"
204
+ >{{ kindLabel(row.kind) }}</span>
205
+ </td>
206
+ <td class="summary">
207
+ {{ row.summary }}
208
+ <small class="mono dim action">{{ row.action }}</small>
209
+ </td>
210
+ <td class="scope">
211
+ <span
212
+ v-if="lineName(row.lineId)"
213
+ class="scope-chip"
214
+ :title="$t('logs.fLine')"
215
+ >{{ lineName(row.lineId) }}</span>
216
+ <span
217
+ v-if="productName(row.productId)"
218
+ class="scope-chip"
219
+ :title="$t('logs.fProduct')"
220
+ >{{ productName(row.productId) }}</span>
221
+ <span
222
+ v-if="recipeName(row.recipeId)"
223
+ class="scope-chip"
224
+ :title="'Recipe'"
225
+ >{{ recipeName(row.recipeId) }}</span>
226
+ <span
227
+ v-if="!row.lineId && !row.productId && !row.recipeId"
228
+ class="dim"
229
+ >—</span>
230
+ </td>
231
+ <td class="detail-cell">
232
+ <button
233
+ v-if="hasDetail(row)"
234
+ class="mini-btn"
235
+ @click="toggle(row)"
236
+ >
237
+ {{ expandedId === row.id ? $t('logs.fold') : $t('logs.expand') }}
238
+ </button>
239
+ <span
240
+ v-else
241
+ class="dim"
242
+ >{{ $t('logs.noDetail') }}</span>
243
+ </td>
244
+ </tr>
245
+ <!-- 详情行内展开:紧贴事件行,点击即见(不再沉到表尾) -->
246
+ <tr v-if="expandedId === row.id">
247
+ <td
248
+ colspan="7"
249
+ class="detail-td"
237
250
  >
238
- {{ expanded === row ? $t('logs.fold') : $t('logs.expand') }}
239
- </button>
240
- <span
241
- v-else
242
- class="dim"
243
- >—</span>
244
- </td>
245
- </tr>
251
+ <pre class="detail-box mono">{{ pretty(row.detailJson) }}</pre>
252
+ </td>
253
+ </tr>
254
+ </template>
246
255
  <tr>
247
256
  <td
248
257
  v-if="opsLog.results.length === 0"
@@ -259,10 +268,6 @@
259
268
  </tr>
260
269
  </tbody>
261
270
  </table>
262
- <pre
263
- v-if="expanded"
264
- class="detail-box mono"
265
- >{{ pretty(expanded.detailJson) }}</pre>
266
271
  </section>
267
272
 
268
273
  <!-- 人工记录弹窗 -->
@@ -284,6 +289,14 @@
284
289
  :placeholder="$t('logs.mSummaryPh')"
285
290
  />
286
291
  </label>
292
+ <label class="m-f">
293
+ <span>{{ $t('logs.mDetail') }}</span>
294
+ <textarea
295
+ v-model="manual.detail"
296
+ rows="2"
297
+ :placeholder="$t('logs.mDetailPh')"
298
+ />
299
+ </label>
287
300
  <div class="m-grid">
288
301
  <label class="m-f">
289
302
  <span>{{ $t('logs.fLine') }}</span>
@@ -385,9 +398,10 @@ const opsLog = useOpsLog()
385
398
  const KINDS = ['write', 'manual', 'alarm', 'line', 'recipe', 'rollback', 'daq', 'system'] as const
386
399
 
387
400
  const q = reactive({ lineId: '', productId: '', recipeId: '', actorKind: '', kind: '', text: '' })
388
- const expanded = ref<OpsLogRow | null>(null)
401
+ /** 当前展开详情的行(audit_log 行 id;null = 全部收起) */
402
+ const expandedId = ref<number | null>(null)
389
403
  const manualOpen = ref(false)
390
- const manual = reactive({ summary: '', lineId: '', productId: '', recipeId: '' })
404
+ const manual = reactive({ summary: '', detail: '', lineId: '', productId: '', recipeId: '' })
391
405
 
392
406
  const hasFilter = computed(() => !!(q.lineId || q.productId || q.recipeId || q.actorKind || q.kind || q.text.trim()))
393
407
  const productsOfLine = computed(() => dcw.products.filter(p => !q.lineId || p.lineId === q.lineId))
@@ -404,7 +418,7 @@ function doQuery(): void {
404
418
  q: q.text.trim(),
405
419
  limit: 300,
406
420
  })
407
- expanded.value = null
421
+ expandedId.value = null
408
422
  }
409
423
 
410
424
  function resetFilters(): void {
@@ -419,6 +433,7 @@ function resetFilters(): void {
419
433
 
420
434
  function openManual(): void {
421
435
  manual.summary = ''
436
+ manual.detail = ''
422
437
  manual.lineId = q.lineId
423
438
  manual.productId = q.productId
424
439
  manual.recipeId = q.recipeId
@@ -427,11 +442,13 @@ function openManual(): void {
427
442
 
428
443
  async function submitManual(): Promise<void> {
429
444
  try {
445
+ const note = manual.detail.trim()
430
446
  await opsLog.postManual({
431
447
  summary: manual.summary.trim(),
432
448
  lineId: manual.lineId,
433
449
  productId: manual.productId,
434
450
  recipeId: manual.recipeId,
451
+ ...(note ? { detail: { note } } : {}),
435
452
  })
436
453
  message.success(tt('logs.mOk'))
437
454
  manualOpen.value = false
@@ -442,8 +459,13 @@ async function submitManual(): Promise<void> {
442
459
  }
443
460
  }
444
461
 
462
+ /** 该行是否带结构化详情(空对象 = 无) */
463
+ function hasDetail(row: OpsLogRow): boolean {
464
+ return !!row.detailJson && row.detailJson !== '{}'
465
+ }
466
+
445
467
  function toggle(row: OpsLogRow): void {
446
- expanded.value = expanded.value === row ? null : row
468
+ expandedId.value = expandedId.value === row.id ? null : row.id
447
469
  }
448
470
 
449
471
  function pretty(json: string): string {
@@ -496,10 +518,8 @@ export default { name: 'OpsLogsPage' }
496
518
 
497
519
  <style scoped>
498
520
  .page { display: flex; flex-direction: column; gap: 12px; }
499
- .head { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; }
500
- .h1 { display: flex; gap: 8px; align-items: center; margin: 0; font-size: 20px; font-weight: 700; color: var(--ink); }
501
- .sub { margin: 4px 0 0; font-size: 12.5px; color: var(--ink-faint); }
502
- .head-actions { display: flex; gap: 10px; align-items: center; }
521
+ .head-actions { display: flex; gap: 10px; align-items: center; padding-bottom: 4px; }
522
+ .sub { margin: 8px 0 0; font-size: 12.5px; color: var(--ink-faint); }
503
523
  .live-dot {
504
524
  width: 8px; height: 8px; border-radius: 50%;
505
525
  background: var(--tone-success-dot);
@@ -530,12 +550,6 @@ export default { name: 'OpsLogsPage' }
530
550
  border: 1px solid var(--glass-line); border-radius: 7px; outline: none;
531
551
  }
532
552
  .inp-sel:focus { border-color: color-mix(in srgb, var(--tone-info-dot) 55%, transparent); }
533
- .mini-btn {
534
- padding: 6px 11px; font-size: 12px; color: var(--ink-soft);
535
- background: var(--frost-bg); border: 1px solid var(--glass-line); border-radius: 7px;
536
- cursor: pointer;
537
- }
538
- .mini-btn:hover { border-color: color-mix(in srgb, var(--tone-info-dot) 45%, transparent); }
539
553
 
540
554
  .table-card {
541
555
  background: var(--surface-glass);
@@ -569,10 +583,12 @@ export default { name: 'OpsLogsPage' }
569
583
  }
570
584
  .detail-cell { white-space: nowrap; }
571
585
  .empty { padding: 22px 0 !important; color: var(--ink-faint); text-align: center; }
586
+ /* 详情行内展开:紧贴事件行的整行 pre 面板 */
587
+ .detail-td { padding: 0 !important; }
572
588
  .detail-box {
573
589
  max-height: 260px; margin: 0; padding: 10px 14px; overflow: auto;
574
- font-size: 11.5px; color: var(--ink-soft);
575
- background: var(--frost-bg); border-top: 1px solid var(--glass-line);
590
+ font-size: 11.5px; color: var(--ink-soft); white-space: pre-wrap; word-break: break-word;
591
+ background: var(--paper-deep); border-top: 1px solid var(--glass-line);
576
592
  }
577
593
  .src-badge {
578
594
  display: inline-block; padding: 1px 8px; font-size: 10.5px;
@@ -616,8 +632,4 @@ export default { name: 'OpsLogsPage' }
616
632
  .m-f textarea:focus, .m-f .inp-sel:focus { border-color: color-mix(in srgb, var(--tone-info-dot) 55%, transparent); }
617
633
  .m-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 8px; }
618
634
  .m-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
619
- .ghost-btn {
620
- padding: 7px 14px; font-size: 12.5px; color: var(--ink-soft);
621
- background: transparent; border: 1px solid var(--glass-line); border-radius: 8px; cursor: pointer;
622
- }
623
635
  </style>
@@ -748,6 +748,7 @@ export default {
748
748
  'thDetail': 'Detail',
749
749
  'expand': 'Detail',
750
750
  'fold': 'Fold',
751
+ 'noDetail': 'None',
751
752
  'loading': 'Querying…',
752
753
  'empty': 'No matching log entries',
753
754
  'src': {
@@ -766,6 +767,8 @@ export default {
766
767
  },
767
768
  'mSummary': 'Entry content',
768
769
  'mSummaryPh': 'e.g. Replaced heater coil of oven #3; extended soaking for current batch by 10 min',
770
+ 'mDetail': 'Detail (optional)',
771
+ 'mDetailPh': 'Context, handling notes, related ticket… (stored in the log detail)',
769
772
  'mSubmit': 'Record',
770
773
  'mOk': 'Recorded',
771
774
  },
@@ -705,6 +705,8 @@ export default {
705
705
  'kind.system': '系统',
706
706
  'mSummary': '记录内容',
707
707
  'mSummaryPh': '如:更换 3 号烘箱加热丝,当前批次延长保温 10 分钟',
708
+ 'mDetail': '详情(可选)',
709
+ 'mDetailPh': '补充说明、处置过程、关联单号等(将存入日志详情)',
708
710
  'mSubmit': '入册',
709
711
  'mOk': '已入册',
710
712
  'src.user': '用户',
@@ -748,6 +750,7 @@ export default {
748
750
  'thDetail': '详情',
749
751
  'expand': '详情',
750
752
  'fold': '收起',
753
+ 'noDetail': '无',
751
754
  'loading': '查询中…',
752
755
  'empty': '无匹配日志',
753
756
  'src': {
@@ -766,6 +769,8 @@ export default {
766
769
  },
767
770
  'mSummary': '记录内容',
768
771
  'mSummaryPh': '如:更换 3 号烘箱加热丝,当前批次延长保温 10 分钟',
772
+ 'mDetail': '详情(可选)',
773
+ 'mDetailPh': '补充说明、处置过程、关联单号等(将存入日志详情)',
769
774
  'mSubmit': '入册',
770
775
  'mOk': '已入册',
771
776
  },