agentworkshop 0.7.22 → 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 (61) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/{BAbAEW0D.js → BJbhwS5o.js} +1 -1
  3. package/.output/public/_nuxt/Bo8t5fkJ.js +1 -0
  4. package/.output/public/_nuxt/{QT54MQV9.js → C53drffV.js} +1 -1
  5. package/.output/public/_nuxt/{0exC4jVL.js → CEHmSufQ.js} +1 -1
  6. package/.output/public/_nuxt/{D6B0xnjk.js → Dc32T41C.js} +1 -1
  7. package/.output/public/_nuxt/{Ci0uX4c4.js → DtzZqC0z.js} +1 -1
  8. package/.output/public/_nuxt/{c3mwkHWP.js → Ko9mMsJj.js} +3 -3
  9. package/.output/public/_nuxt/_id_.C-YXjn8Q.css +1 -0
  10. package/.output/public/_nuxt/_id_.MbzdFygI.css +1 -0
  11. package/.output/public/_nuxt/_wsId_.BqWBlY7X.css +1 -0
  12. package/.output/public/_nuxt/builds/latest.json +1 -1
  13. package/.output/public/_nuxt/builds/meta/1adc96e0-8bbd-4a79-ac7e-b6952556fef8.json +1 -0
  14. package/.output/public/_nuxt/daq.3OKj58HH.css +1 -0
  15. package/.output/public/_nuxt/dcw.rXH2fc2Z.css +1 -0
  16. package/.output/public/_nuxt/entry.OR_psZHp.css +1 -0
  17. package/.output/public/_nuxt/logs.DMOM-iHe.css +1 -0
  18. package/.output/server/chunks/_/nitro.mjs +744 -744
  19. package/.output/server/chunks/build/{_id_-B9RvEQFC.mjs → _id_-DQDqj-cZ.mjs} +31 -31
  20. package/.output/server/chunks/build/{_id_-B9RvEQFC.mjs.map → _id_-DQDqj-cZ.mjs.map} +1 -1
  21. package/.output/server/chunks/build/{_id_-CqF0R5W5.mjs → _id_-DY6v6sqB.mjs} +135 -135
  22. package/.output/server/chunks/build/{_id_-CqF0R5W5.mjs.map → _id_-DY6v6sqB.mjs.map} +1 -1
  23. package/.output/server/chunks/build/{_wsId_-YC2t_fRt.mjs → _wsId_-C2O5zZsI.mjs} +4 -4
  24. package/.output/server/chunks/build/{_wsId_-YC2t_fRt.mjs.map → _wsId_-C2O5zZsI.mjs.map} +1 -1
  25. package/.output/server/chunks/build/{daq-Bhwwz9vp.mjs → daq-B-3xCp_2.mjs} +103 -103
  26. package/.output/server/chunks/build/{daq-Bhwwz9vp.mjs.map → daq-B-3xCp_2.mjs.map} +1 -1
  27. package/.output/server/chunks/build/{dcw-CBPaFj2f.mjs → dcw-BL2dhuz7.mjs} +43 -43
  28. package/.output/server/chunks/build/{dcw-CBPaFj2f.mjs.map → dcw-BL2dhuz7.mjs.map} +1 -1
  29. package/.output/server/chunks/build/{logs-Bz1p6x_5.mjs → logs-Dto_umFY.mjs} +44 -38
  30. package/.output/server/chunks/build/logs-Dto_umFY.mjs.map +1 -0
  31. package/.output/server/chunks/routes/renderer.mjs +1 -1
  32. package/.output/server/chunks/virtual/entry.mjs +14 -6
  33. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  34. package/.output/server/package.json +1 -1
  35. package/app/assets/css/main.css +306 -161
  36. package/app/components/workshop/blocks/ClusterArtifact.vue +0 -16
  37. package/app/pages/daq/[id].vue +919 -938
  38. package/app/pages/daq/index.vue +1 -22
  39. package/app/pages/dcw/[id].vue +1 -4
  40. package/app/pages/dcw/index.vue +1 -30
  41. package/app/pages/logs/index.vue +99 -87
  42. package/i18n/locales/en.ts +3 -0
  43. package/i18n/locales/zh-CN.ts +5 -0
  44. package/package.json +1 -1
  45. package/scripts/_dbg-logs-ui-e2e.py +125 -0
  46. package/server/data/daqs.json +101 -101
  47. package/server/data/dcw-rollback.json +21 -10
  48. package/server/data/dcw-runs.json +25 -27
  49. package/server/data/dcw-writes.json +12 -0
  50. package/server/data/dcws.json +505 -216
  51. package/server/data/line-runs.json +18 -18
  52. package/.output/public/_nuxt/C_2bTK0C.js +0 -1
  53. package/.output/public/_nuxt/_id_.C2VHq_vT.css +0 -1
  54. package/.output/public/_nuxt/_id_.Dswy2p0I.css +0 -1
  55. package/.output/public/_nuxt/_wsId_.BFyCuUwS.css +0 -1
  56. package/.output/public/_nuxt/builds/meta/e2341fc0-6f3f-4680-abf8-935a7c9fb912.json +0 -1
  57. package/.output/public/_nuxt/daq.BTdEun-8.css +0 -1
  58. package/.output/public/_nuxt/dcw.rpqpOYQ8.css +0 -1
  59. package/.output/public/_nuxt/entry.DRUzDwmX.css +0 -1
  60. package/.output/public/_nuxt/logs.BarkrvUp.css +0 -1
  61. package/.output/server/chunks/build/logs-Bz1p6x_5.mjs.map +0 -1
@@ -2225,7 +2225,7 @@ tr.row-recipe-alarm td:first-child { box-shadow: inset 3px 0 0 var(--tone-danger
2225
2225
  .infra-banner .txt a { color: var(--accent); text-decoration: underline; }
2226
2226
  .infra-banner .txt b { font-weight: 600; }
2227
2227
  .infra-banner .txt small { margin-left: 8px; font-size: 10.5px; opacity: 0.75; }
2228
- .infra-banner .pill-btn { flex: 0 0 auto; color: var(--paper-raised); text-decoration: none; }
2228
+ .infra-banner .pill-btn { flex: 0 0 auto; }
2229
2229
 
2230
2230
  /* ---------- 未确认报警条(S5) ---------- */
2231
2231
  .alarm-bar {
@@ -2351,16 +2351,6 @@ tr.row-recipe-alarm td:first-child { box-shadow: inset 3px 0 0 var(--tone-danger
2351
2351
  }
2352
2352
  .hint { font-size: 10px; color: var(--ink-fainter); }
2353
2353
  .test-row { display: flex; gap: 10px; align-items: center; margin: 6px 0 4px; }
2354
- .pill-btn {
2355
- padding: 7px 14px;
2356
- font-size: 12.5px;
2357
- cursor: pointer;
2358
- color: var(--paper-raised);
2359
- background: var(--accent);
2360
- border: 1px solid var(--accent);
2361
- border-radius: var(--radius-pill);
2362
- }
2363
- .pill-btn:disabled { opacity: 0.5; cursor: default; }
2364
2354
  .test-result { font-family: var(--font-mono); font-size: 11px; }
2365
2355
  .test-result.ok { color: var(--tone-success-dot); }
2366
2356
  .test-result.bad { color: var(--tone-danger-dot); }
@@ -2383,17 +2373,6 @@ tr.row-recipe-alarm td:first-child { box-shadow: inset 3px 0 0 var(--tone-danger
2383
2373
  .tpl-table .range { font-size: 11.5px; color: var(--ink-soft); }
2384
2374
  .tpl-table .empty { color: var(--ink-fainter); font-size: 12px; text-align: center; padding: 14px 0; }
2385
2375
  .tpl-table .actions { white-space: nowrap; }
2386
- .mini-btn {
2387
- padding: 3px 10px;
2388
- font-size: 11.5px;
2389
- cursor: pointer;
2390
- color: var(--ink-soft);
2391
- background: var(--paper-deep);
2392
- border: 1px solid var(--line-strong);
2393
- border-radius: var(--radius-chip);
2394
- }
2395
- .mini-btn.danger { color: var(--tone-danger-dot); }
2396
- .mini-btn:hover { border-color: var(--accent); color: var(--accent); }
2397
2376
 
2398
2377
  /* ---------- 节点筛选工具条 ---------- */
2399
2378
  .tbl-toolbar {
@@ -2443,7 +2443,7 @@ h1 { margin: 2px 0 4px; font-size: 30px; font-weight: 400; letter-spacing: -0.01
2443
2443
  .write-row { display: flex; gap: 6px; align-items: center; }
2444
2444
  .write-inp { width: 92px; padding: 4px 8px; }
2445
2445
  .write-btn { padding: 5px 12px; }
2446
- .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; }
2447
2447
  .mini-btn.danger { color: var(--tone-danger-dot); }
2448
2448
  .mini-btn:hover { border-color: var(--accent); color: var(--accent); }
2449
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); }
@@ -2497,9 +2497,6 @@ h1 { margin: 2px 0 4px; font-size: 30px; font-weight: 400; letter-spacing: -0.01
2497
2497
  .ver-diff { margin: 4px 0 0; font-size: 11px; color: var(--ink-faint); }
2498
2498
  .sec-label { margin: 14px 0 6px; font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-faint); }
2499
2499
  .runs { margin-top: 6px; }
2500
- .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); }
2501
- .pill-btn.outline { color: var(--ink); background: var(--paper-raised); border-color: var(--line-strong); }
2502
- .pill-btn:disabled { opacity: 0.5; cursor: default; }
2503
2500
 
2504
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); }
2505
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
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentworkshop",
3
- "version": "0.7.22",
3
+ "version": "0.7.23",
4
4
  "type": "module",
5
5
  "description": "AgentWorkShop 软件开发系统 - 基于 Nuxt 4 的配置驱动运行范式",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -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)